From 36a0e2a6a1218c547e3846170f73ea71615ed392 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 11 Dec 2024 18:55:04 +0530 Subject: [PATCH 01/26] added application add command to create applications --- application/handler/application.go | 34 ++++++ application/handler/handler_test.go | 68 +++++++++++ application/handler/interface.go | 7 ++ application/handler/mock_interface.go | 55 +++++++++ application/service/application.go | 65 ++++++++++ application/service/application_test.go | 153 ++++++++++++++++++++++++ application/service/models.go | 53 ++++++++ application/service/models_test.go | 122 +++++++++++++++++++ main.go | 16 ++- 9 files changed, 569 insertions(+), 4 deletions(-) create mode 100644 application/handler/application.go create mode 100644 application/handler/handler_test.go create mode 100644 application/handler/interface.go create mode 100644 application/handler/mock_interface.go create mode 100644 application/service/application.go create mode 100644 application/service/application_test.go create mode 100644 application/service/models.go create mode 100644 application/service/models_test.go diff --git a/application/handler/application.go b/application/handler/application.go new file mode 100644 index 0000000..79400cc --- /dev/null +++ b/application/handler/application.go @@ -0,0 +1,34 @@ +package handler + +import ( + "errors" + "gofr.dev/pkg/gofr" +) + +var ( + ErrorApplicationNameNotProvided = errors.New("please enter application name, -name=") +) + +type Handler struct { + appAdd ApplicationAdder +} + +func New(appAdd ApplicationAdder) *Handler { + return &Handler{ + appAdd: appAdd, + } +} + +func (h *Handler) Add(ctx *gofr.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, ErrorApplicationNameNotProvided + } + + err := h.appAdd.AddApplication(ctx, name) + if err != nil { + return nil, err + } + + return "Application " + name + " added successfully!", nil +} diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go new file mode 100644 index 0000000..6133938 --- /dev/null +++ b/application/handler/handler_test.go @@ -0,0 +1,68 @@ +package handler + +import ( + "errors" + "go.uber.org/mock/gomock" + "gofr.dev/pkg/gofr/cmd" + + "github.com/stretchr/testify/require" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/container" + "testing" +) + +func TestHandler_Add(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockAppAdder := NewMockApplicationAdder(ctrl) + + testCases := []struct { + name string + appName string + mockCalls []*gomock.Call + expected any + expErr error + }{ + { + name: "success", + appName: "test-app", + mockCalls: []*gomock.Call{ + mockAppAdder.EXPECT().AddApplication(gomock.Any(), "test-app").Return(nil), + }, + expected: "Application test-app added successfully!", + expErr: nil, + }, + { + name: "missing name parameter", + appName: "", + expected: nil, + expErr: ErrorApplicationNameNotProvided, + }, + { + name: "error adding application", + appName: "test-app", + mockCalls: []*gomock.Call{ + mockAppAdder.EXPECT().AddApplication(gomock.Any(), "test-app").Return(errors.New("internal error")), + }, + expected: nil, + expErr: errors.New("internal error"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockCont, _ := container.NewMockContainer(t) + ctx := &gofr.Context{ + Container: mockCont, + Request: cmd.NewRequest([]string{"", "-name=" + tc.appName}), + } + + h := New(mockAppAdder) + res, err := h.Add(ctx) + + require.Equal(t, tc.expErr, err) + require.Equal(t, tc.expected, res) + }) + } +} diff --git a/application/handler/interface.go b/application/handler/interface.go new file mode 100644 index 0000000..fb29e0e --- /dev/null +++ b/application/handler/interface.go @@ -0,0 +1,7 @@ +package handler + +import "gofr.dev/pkg/gofr" + +type ApplicationAdder interface { + AddApplication(ctx *gofr.Context, name string) error +} diff --git a/application/handler/mock_interface.go b/application/handler/mock_interface.go new file mode 100644 index 0000000..1435ad3 --- /dev/null +++ b/application/handler/mock_interface.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -source=interface.go -destination=mock_interface.go -package=handler +// + +// Package handler is a generated GoMock package. +package handler + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + gofr "gofr.dev/pkg/gofr" +) + +// MockApplicationAdder is a mock of ApplicationAdder interface. +type MockApplicationAdder struct { + ctrl *gomock.Controller + recorder *MockApplicationAdderMockRecorder + isgomock struct{} +} + +// MockApplicationAdderMockRecorder is the mock recorder for MockApplicationAdder. +type MockApplicationAdderMockRecorder struct { + mock *MockApplicationAdder +} + +// NewMockApplicationAdder creates a new mock instance. +func NewMockApplicationAdder(ctrl *gomock.Controller) *MockApplicationAdder { + mock := &MockApplicationAdder{ctrl: ctrl} + mock.recorder = &MockApplicationAdderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockApplicationAdder) EXPECT() *MockApplicationAdderMockRecorder { + return m.recorder +} + +// AddApplication mocks base method. +func (m *MockApplicationAdder) AddApplication(ctx *gofr.Context, name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddApplication", ctx, name) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddApplication indicates an expected call of AddApplication. +func (mr *MockApplicationAdderMockRecorder) AddApplication(ctx, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddApplication", reflect.TypeOf((*MockApplicationAdder)(nil).AddApplication), ctx, name) +} diff --git a/application/service/application.go b/application/service/application.go new file mode 100644 index 0000000..3456abf --- /dev/null +++ b/application/service/application.go @@ -0,0 +1,65 @@ +package service + +import ( + "encoding/json" + "fmt" + "net/http" + + "gofr.dev/pkg/gofr" +) + +type Service struct { +} + +func New() *Service { + return &Service{} +} + +func (s *Service) AddApplication(ctx *gofr.Context, name string) error { + var ( + envs []Environment + input string + ) + + app := &Application{Name: name} + api := ctx.GetHTTPService("api-service") + order := 1 + + ctx.Out.Print("Do you wish to add environments to the application? (y/n) ") + + fmt.Scanf("%s", &input) + + for { + if input == "y" { + ctx.Out.Println("Enter environment name:") + fmt.Scanf("%s", &input) + + envs = append(envs, Environment{Name: input, Order: order}) + order++ + } else { + break + } + + ctx.Out.Print("Do you wish to add more? (y/n) ") + fmt.Scanf("%s", &input) + if input == "n" { + break + } + } + + app.Envs = envs + body, _ := json.Marshal(app) + + resp, err := api.PostWithHeaders(ctx, "application", nil, body, nil) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return getAPIError(resp) + } + + return nil +} diff --git a/application/service/application_test.go b/application/service/application_test.go new file mode 100644 index 0000000..0bec1d2 --- /dev/null +++ b/application/service/application_test.go @@ -0,0 +1,153 @@ +package service + +import ( + "bytes" + "encoding/json" + "errors" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/cmd/terminal" + "gofr.dev/pkg/gofr/container" + "gofr.dev/pkg/gofr/service" + "io" + "net/http" + "os" + "testing" +) + +var errAPICall = errors.New("error in API call") + +func Test_AddApplication(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCont, mocks := container.NewMockContainer(t, func(c *container.Container, ctrl *gomock.Controller) any { + return service.NewMockHTTP(ctrl) + }) + mockCont.Services["api-service"] = mocks.HTTPService + ctx := &gofr.Context{Container: mockCont, Out: terminal.New()} + + b, err := json.Marshal(MockErrorResponse{Error: "Something went wrong"}) + if err != nil { + t.Fatalf("Failed to marshal test response body: %v", err) + } + + testCases := []struct { + name string + mockCalls []*gomock.Call + expError error + }{ + { + name: "success Post call", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + Return(&http.Response{StatusCode: http.StatusCreated, Body: io.NopCloser(&errorReader{})}, nil), + }, + expError: nil, + }, + { + name: "error in Post call", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + Return(nil, errAPICall), + }, + expError: errAPICall, + }, + { + name: "unexpected response", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + Return(&http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(bytes.NewBuffer(b))}, nil), + }, + expError: &ErrAPIService{StatusCode: http.StatusInternalServerError, Message: "Something went wrong"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + s := New() + + errSvc := s.AddApplication(ctx, "test") + + require.Equal(t, tt.expError, errSvc) + }) + } +} + +func Test_AddApplication_WithEnvs(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCont, mocks := container.NewMockContainer(t, func(c *container.Container, ctrl *gomock.Controller) any { + return service.NewMockHTTP(ctrl) + }) + + mockCont.Services["api-service"] = mocks.HTTPService + ctx := &gofr.Context{Container: mockCont, Out: terminal.New()} + + testCases := []struct { + name string + mockCalls []*gomock.Call + userInput string + expectedEnvs []Environment + expError error + }{ + { + name: "success with environments", + userInput: "y\nprod\ny\ndev\nn\n", + expectedEnvs: []Environment{ + {Name: "prod", Order: 1}, + {Name: "dev", Order: 2}, + }, + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + DoAndReturn(func(ctx *gofr.Context, endpoint string, headers, body, query interface{}) (*http.Response, error) { + var app Application + _ = json.Unmarshal(body.([]byte), &app) + require.Equal(t, "test", app.Name) + require.Equal(t, []Environment{ + {Name: "prod", Order: 1}, + {Name: "dev", Order: 2}, + }, app.Envs) + return &http.Response{StatusCode: http.StatusCreated, Body: io.NopCloser(bytes.NewBuffer(nil))}, nil + }), + }, + expError: nil, + }, + { + name: "no environments added", + userInput: "n\n", + expectedEnvs: []Environment{}, + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + DoAndReturn(func(ctx *gofr.Context, endpoint string, headers, body, query interface{}) (*http.Response, error) { + var app Application + _ = json.Unmarshal(body.([]byte), &app) + require.Equal(t, "test", app.Name) + require.Empty(t, app.Envs) + return &http.Response{StatusCode: http.StatusCreated, Body: io.NopCloser(bytes.NewBuffer(nil))}, nil + }), + }, + expError: nil, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + s := New() + + // Mock user input + r, w, _ := os.Pipe() + w.Write([]byte(tt.userInput)) + + oldStdin := os.Stdin + os.Stdin = r + + defer func() { os.Stdin = oldStdin }() + + errSvc := s.AddApplication(ctx, "test") + require.Equal(t, tt.expError, errSvc) + }) + } +} diff --git a/application/service/models.go b/application/service/models.go new file mode 100644 index 0000000..44fe149 --- /dev/null +++ b/application/service/models.go @@ -0,0 +1,53 @@ +package service + +import ( + "encoding/json" + "io" + "net/http" +) + +type ErrAPIService struct { + StatusCode int + Message string +} + +func (e *ErrAPIService) Error() string { + return e.Message +} + +var errInternal = &ErrAPIService{ + StatusCode: http.StatusInternalServerError, + Message: "error in POST /application zop-api, invalid response", +} + +func getAPIError(resp *http.Response) *ErrAPIService { + var errResp struct { + Error string `json:"error"` + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return errInternal + } + + err = json.Unmarshal(b, &errResp) + if err != nil { + return errInternal + } + + return &ErrAPIService{ + StatusCode: resp.StatusCode, + Message: errResp.Error, + } +} + +type Environment struct { + Name string `json:"name"` + Order int `json:"order"` + DeploymentSpace any `json:"deploymentSpace,omitempty"` +} + +type Application struct { + Name string `json:"name"` + Envs []Environment `json:"environments,omitempty"` +} diff --git a/application/service/models_test.go b/application/service/models_test.go new file mode 100644 index 0000000..f67aecd --- /dev/null +++ b/application/service/models_test.go @@ -0,0 +1,122 @@ +package service + +import ( + "bytes" + "encoding/json" + "github.com/stretchr/testify/require" + "io" + "net/http" + "testing" +) + +func TestGetAPIError(t *testing.T) { + tests := []struct { + name string + responseBody interface{} + responseCode int + expectError *ErrAPIService + }{ + { + name: "Valid error response", + responseBody: MockErrorResponse{ + Error: "Something went wrong", + }, + responseCode: http.StatusBadRequest, + expectError: &ErrAPIService{ + StatusCode: http.StatusBadRequest, + Message: "Something went wrong", + }, + }, + { + name: "Malformed JSON response", + responseBody: "{error: unquoted string}", + responseCode: http.StatusBadRequest, + expectError: errInternal, + }, + { + name: "Empty response body", + responseBody: "", + responseCode: http.StatusBadRequest, + expectError: errInternal, + }, + { + name: "Error reading body", + responseBody: nil, + responseCode: http.StatusInternalServerError, + expectError: errInternal, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var bodyReader io.ReadCloser + + if tt.responseBody == nil { + // Simulate error reading body by providing a reader that always errors + bodyReader = io.NopCloser(&errorReader{}) + } else { + b, err := json.Marshal(tt.responseBody) + if err != nil { + t.Fatalf("Failed to marshal test response body: %v", err) + } + + bodyReader = io.NopCloser(bytes.NewBuffer(b)) + } + + // Create a mock response + resp := &http.Response{ + StatusCode: tt.responseCode, + Body: bodyReader, + } + + // Call the function and check results + actualError := getAPIError(resp) + + require.Equal(t, tt.expectError.StatusCode, actualError.StatusCode, "Unexpected status code") + require.Equal(t, tt.expectError.Message, actualError.Message, "Unexpected error message") + }) + } +} + +type MockErrorResponse struct { + Error string `json:"error"` +} + +type errorReader struct{} + +func (e *errorReader) Read(p []byte) (n int, err error) { + return 0, io.ErrUnexpectedEOF +} + +func (e *errorReader) Close() error { + return nil +} + +func Test_ErrAPIError(t *testing.T) { + tests := []struct { + name string + err *ErrAPIService + }{ + { + name: "Valid error", + err: &ErrAPIService{ + StatusCode: http.StatusBadRequest, + Message: "Something went wrong", + }, + }, + { + name: "Empty error message", + err: &ErrAPIService{ + StatusCode: http.StatusBadRequest, + Message: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.err.Error() + require.Equal(t, tt.err.Message, actual, "Unexpected error message") + }) + } +} diff --git a/main.go b/main.go index fff82e9..a2426d1 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,12 @@ import ( "os" "path/filepath" - _ "github.com/mattn/go-sqlite3" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/service" + _ "modernc.org/sqlite" + applicationHandler "zop.dev/cli/zop/application/handler" + applicationSvc "zop.dev/cli/zop/application/service" impHandler "zop.dev/cli/zop/cloud/handler" impService "zop.dev/cli/zop/cloud/service/gcp" listSvc "zop.dev/cli/zop/cloud/service/list" @@ -42,13 +44,19 @@ func main() { } defer db.Close() - aStore := impStore.New(db) - aSvc := impService.New(aStore) + accStore := impStore.New() + accSvc := impService.New(accStore) lSvc := listSvc.New() - h := impHandler.New(aSvc, lSvc) + h := impHandler.New(accSvc, lSvc) app.SubCommand("cloud import", h.Import) app.SubCommand("cloud list", h.List) + app.SubCommand("cloud select", h.Set) + + appSvc := applicationSvc.New() + appH := applicationHandler.New(appSvc) + + app.SubCommand("application add", appH.Add) app.Run() } From 7bc24e2b99633bfa68e3f1ed5b53911c6dda6e5e Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 11 Dec 2024 19:07:01 +0530 Subject: [PATCH 02/26] fix linters --- application/handler/application.go | 1 + application/handler/handler_test.go | 12 +++++++----- application/service/application.go | 22 ++++++++++++---------- application/service/application_test.go | 19 ++++++++++--------- application/service/models_test.go | 7 ++++--- main.go | 2 +- 6 files changed, 35 insertions(+), 28 deletions(-) diff --git a/application/handler/application.go b/application/handler/application.go index 79400cc..65b9dbd 100644 --- a/application/handler/application.go +++ b/application/handler/application.go @@ -2,6 +2,7 @@ package handler import ( "errors" + "gofr.dev/pkg/gofr" ) diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go index 6133938..deb430b 100644 --- a/application/handler/handler_test.go +++ b/application/handler/handler_test.go @@ -2,15 +2,17 @@ package handler import ( "errors" - "go.uber.org/mock/gomock" - "gofr.dev/pkg/gofr/cmd" + "testing" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/cmd" "gofr.dev/pkg/gofr/container" - "testing" ) +var errAPICall = errors.New("error in API call") + func TestHandler_Add(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -43,10 +45,10 @@ func TestHandler_Add(t *testing.T) { name: "error adding application", appName: "test-app", mockCalls: []*gomock.Call{ - mockAppAdder.EXPECT().AddApplication(gomock.Any(), "test-app").Return(errors.New("internal error")), + mockAppAdder.EXPECT().AddApplication(gomock.Any(), "test-app").Return(errAPICall), }, expected: nil, - expErr: errors.New("internal error"), + expErr: errAPICall, }, } diff --git a/application/service/application.go b/application/service/application.go index 3456abf..afd0e64 100644 --- a/application/service/application.go +++ b/application/service/application.go @@ -15,7 +15,7 @@ func New() *Service { return &Service{} } -func (s *Service) AddApplication(ctx *gofr.Context, name string) error { +func (*Service) AddApplication(ctx *gofr.Context, name string) error { var ( envs []Environment input string @@ -27,21 +27,23 @@ func (s *Service) AddApplication(ctx *gofr.Context, name string) error { ctx.Out.Print("Do you wish to add environments to the application? (y/n) ") - fmt.Scanf("%s", &input) + _, _ = fmt.Scanf("%s", &input) for { - if input == "y" { - ctx.Out.Println("Enter environment name:") - fmt.Scanf("%s", &input) - - envs = append(envs, Environment{Name: input, Order: order}) - order++ - } else { + if input != "y" { break } + ctx.Out.Print("Enter environment name: ") + + _, _ = fmt.Scanf("%s", &input) + envs = append(envs, Environment{Name: input, Order: order}) + order++ + ctx.Out.Print("Do you wish to add more? (y/n) ") - fmt.Scanf("%s", &input) + + _, _ = fmt.Scanf("%s", &input) + if input == "n" { break } diff --git a/application/service/application_test.go b/application/service/application_test.go index 0bec1d2..1642983 100644 --- a/application/service/application_test.go +++ b/application/service/application_test.go @@ -4,16 +4,17 @@ import ( "bytes" "encoding/json" "errors" + "io" + "net/http" + "os" + "testing" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/cmd/terminal" "gofr.dev/pkg/gofr/container" "gofr.dev/pkg/gofr/service" - "io" - "net/http" - "os" - "testing" ) var errAPICall = errors.New("error in API call") @@ -22,7 +23,7 @@ func Test_AddApplication(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockCont, mocks := container.NewMockContainer(t, func(c *container.Container, ctrl *gomock.Controller) any { + mockCont, mocks := container.NewMockContainer(t, func(_ *container.Container, ctrl *gomock.Controller) any { return service.NewMockHTTP(ctrl) }) mockCont.Services["api-service"] = mocks.HTTPService @@ -79,7 +80,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockCont, mocks := container.NewMockContainer(t, func(c *container.Container, ctrl *gomock.Controller) any { + mockCont, mocks := container.NewMockContainer(t, func(_ *container.Container, ctrl *gomock.Controller) any { return service.NewMockHTTP(ctrl) }) @@ -102,7 +103,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { }, mockCalls: []*gomock.Call{ mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). - DoAndReturn(func(ctx *gofr.Context, endpoint string, headers, body, query interface{}) (*http.Response, error) { + DoAndReturn(func(body any) (*http.Response, error) { var app Application _ = json.Unmarshal(body.([]byte), &app) require.Equal(t, "test", app.Name) @@ -121,7 +122,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { expectedEnvs: []Environment{}, mockCalls: []*gomock.Call{ mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). - DoAndReturn(func(ctx *gofr.Context, endpoint string, headers, body, query interface{}) (*http.Response, error) { + DoAndReturn(func(body any) (*http.Response, error) { var app Application _ = json.Unmarshal(body.([]byte), &app) require.Equal(t, "test", app.Name) @@ -139,7 +140,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { // Mock user input r, w, _ := os.Pipe() - w.Write([]byte(tt.userInput)) + _, _ = w.WriteString(tt.userInput) oldStdin := os.Stdin os.Stdin = r diff --git a/application/service/models_test.go b/application/service/models_test.go index f67aecd..2dbc281 100644 --- a/application/service/models_test.go +++ b/application/service/models_test.go @@ -3,10 +3,11 @@ package service import ( "bytes" "encoding/json" - "github.com/stretchr/testify/require" "io" "net/http" "testing" + + "github.com/stretchr/testify/require" ) func TestGetAPIError(t *testing.T) { @@ -84,11 +85,11 @@ type MockErrorResponse struct { type errorReader struct{} -func (e *errorReader) Read(p []byte) (n int, err error) { +func (*errorReader) Read(_ []byte) (n int, err error) { return 0, io.ErrUnexpectedEOF } -func (e *errorReader) Close() error { +func (*errorReader) Close() error { return nil } diff --git a/main.go b/main.go index a2426d1..e036e5c 100644 --- a/main.go +++ b/main.go @@ -44,7 +44,7 @@ func main() { } defer db.Close() - accStore := impStore.New() + accStore := impStore.New(db) accSvc := impService.New(accStore) lSvc := listSvc.New() h := impHandler.New(accSvc, lSvc) From 650e5e07ad4c05bd4759687fc7643ad51eda926d Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Thu, 12 Dec 2024 11:19:34 +0530 Subject: [PATCH 03/26] fix tests for application --- application/service/application_test.go | 4 ++-- main.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/application/service/application_test.go b/application/service/application_test.go index 1642983..70c46ec 100644 --- a/application/service/application_test.go +++ b/application/service/application_test.go @@ -103,7 +103,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { }, mockCalls: []*gomock.Call{ mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). - DoAndReturn(func(body any) (*http.Response, error) { + DoAndReturn(func(_ *gofr.Context, _ string, _, body, _ interface{}) (*http.Response, error) { var app Application _ = json.Unmarshal(body.([]byte), &app) require.Equal(t, "test", app.Name) @@ -122,7 +122,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { expectedEnvs: []Environment{}, mockCalls: []*gomock.Call{ mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). - DoAndReturn(func(body any) (*http.Response, error) { + DoAndReturn(func(_ *gofr.Context, _ string, _, body, _ interface{}) (*http.Response, error) { var app Application _ = json.Unmarshal(body.([]byte), &app) require.Equal(t, "test", app.Name) diff --git a/main.go b/main.go index e036e5c..7713c0a 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,6 @@ func main() { app.SubCommand("cloud import", h.Import) app.SubCommand("cloud list", h.List) - app.SubCommand("cloud select", h.Set) appSvc := applicationSvc.New() appH := applicationHandler.New(appSvc) From 6b4f023ed3dba49849ce91d92fe1437f3348bb76 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Thu, 12 Dec 2024 16:57:41 +0530 Subject: [PATCH 04/26] add application lister command to display all applications and environments --- application/handler/application.go | 32 +++++++++++++- application/handler/handler_test.go | 61 ++++++++++++++++++++++++++- application/handler/interface.go | 8 +++- application/handler/mock_interface.go | 44 +++++++++++++------ application/service/application.go | 24 +++++++++++ application/service/models.go | 1 + main.go | 1 + 7 files changed, 152 insertions(+), 19 deletions(-) diff --git a/application/handler/application.go b/application/handler/application.go index 65b9dbd..e518c81 100644 --- a/application/handler/application.go +++ b/application/handler/application.go @@ -2,6 +2,9 @@ package handler import ( "errors" + "fmt" + "sort" + "strings" "gofr.dev/pkg/gofr" ) @@ -11,10 +14,10 @@ var ( ) type Handler struct { - appAdd ApplicationAdder + appAdd ApplicationService } -func New(appAdd ApplicationAdder) *Handler { +func New(appAdd ApplicationService) *Handler { return &Handler{ appAdd: appAdd, } @@ -33,3 +36,28 @@ func (h *Handler) Add(ctx *gofr.Context) (any, error) { return "Application " + name + " added successfully!", nil } + +func (h *Handler) List(ctx *gofr.Context) (any, error) { + apps, err := h.appAdd.GetApplications(ctx) + if err != nil { + return nil, err + } + + ctx.Out.Println("Applications and their environments:") + + s := strings.Builder{} + for _, app := range apps { + s.WriteString(fmt.Sprintf("%s ", app.Name)) + + sort.Slice(app.Envs, func(i, j int) bool { return app.Envs[i].Order < app.Envs[j].Order }) + + for _, env := range app.Envs { + s.WriteString(fmt.Sprintf("%s > ", env.Name)) + } + + ctx.Out.Println(s.String()[:s.Len()-2]) + s.Reset() + } + + return nil, nil +} diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go index deb430b..94e6dda 100644 --- a/application/handler/handler_test.go +++ b/application/handler/handler_test.go @@ -2,7 +2,10 @@ package handler import ( "errors" + "gofr.dev/pkg/gofr/cmd/terminal" + "gofr.dev/pkg/gofr/testutil" "testing" + svc "zop.dev/cli/zop/application/service" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -17,7 +20,7 @@ func TestHandler_Add(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockAppAdder := NewMockApplicationAdder(ctrl) + mockAppAdder := NewMockApplicationService(ctrl) testCases := []struct { name string @@ -68,3 +71,59 @@ func TestHandler_Add(t *testing.T) { }) } } + +func TestHandler_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := NewMockApplicationService(ctrl) + + testCases := []struct { + name string + mockCalls []*gomock.Call + expected string + expErr error + }{ + { + name: "success", + mockCalls: []*gomock.Call{ + mockSvc.EXPECT().GetApplications(gomock.Any()). + Return([]svc.Application{ + {1, "app1", + []svc.Environment{{"env1", 1, nil}, {"env2", 2, nil}}}, + {2, "app2", + []svc.Environment{{"dev", 1, nil}, {"prod", 2, nil}}}, + }, nil), + }, + expected: "Applications and their environments:\napp1 env1 > env2 \napp2 dev > prod \n", + }, + { + name: "failure", + mockCalls: []*gomock.Call{ + mockSvc.EXPECT().GetApplications(gomock.Any()). + Return(nil, errAPICall), + }, + expErr: errAPICall, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + h := New(mockSvc) + out := testutil.StdoutOutputForFunc(func() { + ctx := &gofr.Context{ + Request: cmd.NewRequest([]string{""}), + Out: terminal.New(), + } + + _, err := h.List(ctx) + + require.Equal(t, tc.expErr, err) + }) + + require.Equal(t, tc.expected, out) + }) + } +} diff --git a/application/handler/interface.go b/application/handler/interface.go index fb29e0e..9ed4284 100644 --- a/application/handler/interface.go +++ b/application/handler/interface.go @@ -1,7 +1,11 @@ package handler -import "gofr.dev/pkg/gofr" +import ( + "gofr.dev/pkg/gofr" + "zop.dev/cli/zop/application/service" +) -type ApplicationAdder interface { +type ApplicationService interface { AddApplication(ctx *gofr.Context, name string) error + GetApplications(ctx *gofr.Context) ([]service.Application, error) } diff --git a/application/handler/mock_interface.go b/application/handler/mock_interface.go index 1435ad3..7340b69 100644 --- a/application/handler/mock_interface.go +++ b/application/handler/mock_interface.go @@ -14,34 +14,35 @@ import ( gomock "go.uber.org/mock/gomock" gofr "gofr.dev/pkg/gofr" + service "zop.dev/cli/zop/application/service" ) -// MockApplicationAdder is a mock of ApplicationAdder interface. -type MockApplicationAdder struct { +// MockApplicationService is a mock of ApplicationService interface. +type MockApplicationService struct { ctrl *gomock.Controller - recorder *MockApplicationAdderMockRecorder + recorder *MockApplicationServiceMockRecorder isgomock struct{} } -// MockApplicationAdderMockRecorder is the mock recorder for MockApplicationAdder. -type MockApplicationAdderMockRecorder struct { - mock *MockApplicationAdder +// MockApplicationServiceMockRecorder is the mock recorder for MockApplicationService. +type MockApplicationServiceMockRecorder struct { + mock *MockApplicationService } -// NewMockApplicationAdder creates a new mock instance. -func NewMockApplicationAdder(ctrl *gomock.Controller) *MockApplicationAdder { - mock := &MockApplicationAdder{ctrl: ctrl} - mock.recorder = &MockApplicationAdderMockRecorder{mock} +// NewMockApplicationService creates a new mock instance. +func NewMockApplicationService(ctrl *gomock.Controller) *MockApplicationService { + mock := &MockApplicationService{ctrl: ctrl} + mock.recorder = &MockApplicationServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockApplicationAdder) EXPECT() *MockApplicationAdderMockRecorder { +func (m *MockApplicationService) EXPECT() *MockApplicationServiceMockRecorder { return m.recorder } // AddApplication mocks base method. -func (m *MockApplicationAdder) AddApplication(ctx *gofr.Context, name string) error { +func (m *MockApplicationService) AddApplication(ctx *gofr.Context, name string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddApplication", ctx, name) ret0, _ := ret[0].(error) @@ -49,7 +50,22 @@ func (m *MockApplicationAdder) AddApplication(ctx *gofr.Context, name string) er } // AddApplication indicates an expected call of AddApplication. -func (mr *MockApplicationAdderMockRecorder) AddApplication(ctx, name any) *gomock.Call { +func (mr *MockApplicationServiceMockRecorder) AddApplication(ctx, name any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddApplication", reflect.TypeOf((*MockApplicationAdder)(nil).AddApplication), ctx, name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddApplication", reflect.TypeOf((*MockApplicationService)(nil).AddApplication), ctx, name) +} + +// GetApplications mocks base method. +func (m *MockApplicationService) GetApplications(ctx *gofr.Context) ([]service.Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetApplications", ctx) + ret0, _ := ret[0].([]service.Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetApplications indicates an expected call of GetApplications. +func (mr *MockApplicationServiceMockRecorder) GetApplications(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplications", reflect.TypeOf((*MockApplicationService)(nil).GetApplications), ctx) } diff --git a/application/service/application.go b/application/service/application.go index afd0e64..a9958a6 100644 --- a/application/service/application.go +++ b/application/service/application.go @@ -3,6 +3,7 @@ package service import ( "encoding/json" "fmt" + "io" "net/http" "gofr.dev/pkg/gofr" @@ -65,3 +66,26 @@ func (*Service) AddApplication(ctx *gofr.Context, name string) error { return nil } + +func (*Service) GetApplications(ctx *gofr.Context) ([]Application, error) { + api := ctx.GetHTTPService("api-service") + + reps, err := api.Get(ctx, "applications", nil) + if err != nil { + return nil, err + } + defer reps.Body.Close() + + var apps struct { + Data []Application `json:"data"` + } + + body, _ := io.ReadAll(reps.Body) + + err = json.Unmarshal(body, &apps) + if err != nil { + return nil, err + } + + return apps.Data, nil +} diff --git a/application/service/models.go b/application/service/models.go index 44fe149..d8715f7 100644 --- a/application/service/models.go +++ b/application/service/models.go @@ -48,6 +48,7 @@ type Environment struct { } type Application struct { + ID int `json:"id"` Name string `json:"name"` Envs []Environment `json:"environments,omitempty"` } diff --git a/main.go b/main.go index 7713c0a..7c36332 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,7 @@ func main() { appH := applicationHandler.New(appSvc) app.SubCommand("application add", appH.Add) + app.SubCommand("application list", appH.List) app.Run() } From dbd42c3f92f2c2110cc1ffc9434503cb9dc87833 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Thu, 12 Dec 2024 18:08:41 +0530 Subject: [PATCH 05/26] modify the application list output --- application/handler/application.go | 14 ++++++++++---- application/handler/handler_test.go | 4 +++- application/service/application.go | 4 +++- main.go | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/application/handler/application.go b/application/handler/application.go index e518c81..c56764f 100644 --- a/application/handler/application.go +++ b/application/handler/application.go @@ -3,6 +3,7 @@ package handler import ( "errors" "fmt" + "gofr.dev/pkg/gofr/cmd/terminal" "sort" "strings" @@ -43,11 +44,14 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { return nil, err } - ctx.Out.Println("Applications and their environments:") + ctx.Out.Println("Applications and their environments:\n") s := strings.Builder{} - for _, app := range apps { - s.WriteString(fmt.Sprintf("%s ", app.Name)) + for i, app := range apps { + ctx.Out.Printf("%d.", i+1) + ctx.Out.SetColor(terminal.Cyan) + ctx.Out.Printf(" %s \n\t", app.Name) + ctx.Out.ResetColor() sort.Slice(app.Envs, func(i, j int) bool { return app.Envs[i].Order < app.Envs[j].Order }) @@ -55,9 +59,11 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { s.WriteString(fmt.Sprintf("%s > ", env.Name)) } + ctx.Out.SetColor(terminal.Green) ctx.Out.Println(s.String()[:s.Len()-2]) + ctx.Out.ResetColor() s.Reset() } - return nil, nil + return "\n", nil } diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go index 94e6dda..a6bab64 100644 --- a/application/handler/handler_test.go +++ b/application/handler/handler_test.go @@ -95,7 +95,9 @@ func TestHandler_List(t *testing.T) { []svc.Environment{{"dev", 1, nil}, {"prod", 2, nil}}}, }, nil), }, - expected: "Applications and their environments:\napp1 env1 > env2 \napp2 dev > prod \n", + expected: "Applications and their environments:\n\n1.\x1b[38;5;6m app1 " + + "\n\t\x1b[0m\x1b[38;5;2menv1 > env2 \n\x1b[0m2.\x1b[38;5;6m app2 " + + "\n\t\x1b[0m\x1b[38;5;2mdev > prod \n\x1b[0m", }, { name: "failure", diff --git a/application/service/application.go b/application/service/application.go index a9958a6..445354d 100644 --- a/application/service/application.go +++ b/application/service/application.go @@ -53,7 +53,9 @@ func (*Service) AddApplication(ctx *gofr.Context, name string) error { app.Envs = envs body, _ := json.Marshal(app) - resp, err := api.PostWithHeaders(ctx, "application", nil, body, nil) + resp, err := api.PostWithHeaders(ctx, "applications", nil, body, map[string]string{ + "Content-Type": "application/json", + }) if err != nil { return err } diff --git a/main.go b/main.go index 7c36332..010aa3a 100644 --- a/main.go +++ b/main.go @@ -6,9 +6,9 @@ import ( "os" "path/filepath" + _ "github.com/mattn/go-sqlite3" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/service" - _ "modernc.org/sqlite" applicationHandler "zop.dev/cli/zop/application/handler" applicationSvc "zop.dev/cli/zop/application/service" From 59e1868979d40f365d9884a780149db62f8bfea5 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Thu, 12 Dec 2024 18:08:41 +0530 Subject: [PATCH 06/26] modify the application list output --- application/handler/application.go | 14 ++++++++++---- application/handler/handler_test.go | 4 +++- application/service/application.go | 4 +++- go.mod | 2 +- main.go | 2 +- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/application/handler/application.go b/application/handler/application.go index e518c81..c56764f 100644 --- a/application/handler/application.go +++ b/application/handler/application.go @@ -3,6 +3,7 @@ package handler import ( "errors" "fmt" + "gofr.dev/pkg/gofr/cmd/terminal" "sort" "strings" @@ -43,11 +44,14 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { return nil, err } - ctx.Out.Println("Applications and their environments:") + ctx.Out.Println("Applications and their environments:\n") s := strings.Builder{} - for _, app := range apps { - s.WriteString(fmt.Sprintf("%s ", app.Name)) + for i, app := range apps { + ctx.Out.Printf("%d.", i+1) + ctx.Out.SetColor(terminal.Cyan) + ctx.Out.Printf(" %s \n\t", app.Name) + ctx.Out.ResetColor() sort.Slice(app.Envs, func(i, j int) bool { return app.Envs[i].Order < app.Envs[j].Order }) @@ -55,9 +59,11 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { s.WriteString(fmt.Sprintf("%s > ", env.Name)) } + ctx.Out.SetColor(terminal.Green) ctx.Out.Println(s.String()[:s.Len()-2]) + ctx.Out.ResetColor() s.Reset() } - return nil, nil + return "\n", nil } diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go index 94e6dda..a6bab64 100644 --- a/application/handler/handler_test.go +++ b/application/handler/handler_test.go @@ -95,7 +95,9 @@ func TestHandler_List(t *testing.T) { []svc.Environment{{"dev", 1, nil}, {"prod", 2, nil}}}, }, nil), }, - expected: "Applications and their environments:\napp1 env1 > env2 \napp2 dev > prod \n", + expected: "Applications and their environments:\n\n1.\x1b[38;5;6m app1 " + + "\n\t\x1b[0m\x1b[38;5;2menv1 > env2 \n\x1b[0m2.\x1b[38;5;6m app2 " + + "\n\t\x1b[0m\x1b[38;5;2mdev > prod \n\x1b[0m", }, { name: "failure", diff --git a/application/service/application.go b/application/service/application.go index a9958a6..445354d 100644 --- a/application/service/application.go +++ b/application/service/application.go @@ -53,7 +53,9 @@ func (*Service) AddApplication(ctx *gofr.Context, name string) error { app.Envs = envs body, _ := json.Marshal(app) - resp, err := api.PostWithHeaders(ctx, "application", nil, body, nil) + resp, err := api.PostWithHeaders(ctx, "applications", nil, body, map[string]string{ + "Content-Type": "application/json", + }) if err != nil { return err } diff --git a/go.mod b/go.mod index 179cbf8..92e7d9d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.8 require ( github.com/mattn/go-sqlite3 v1.14.24 github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 gofr.dev v1.28.0 golang.org/x/oauth2 v0.24.0 @@ -62,7 +63,6 @@ require ( github.com/redis/go-redis/v9 v9.7.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/segmentio/kafka-go v0.4.47 // indirect - github.com/stretchr/testify v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect diff --git a/main.go b/main.go index 7c36332..010aa3a 100644 --- a/main.go +++ b/main.go @@ -6,9 +6,9 @@ import ( "os" "path/filepath" + _ "github.com/mattn/go-sqlite3" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/service" - _ "modernc.org/sqlite" applicationHandler "zop.dev/cli/zop/application/handler" applicationSvc "zop.dev/cli/zop/application/service" From e1bbd867d8ff13a0d0312101fbfd684f71cb8aaa Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Thu, 12 Dec 2024 18:18:54 +0530 Subject: [PATCH 07/26] add environment creation --- environment/handler/env.go | 24 +++++ environment/handler/interface.go | 7 ++ environment/service/app_selector.go | 102 ++++++++++++++++++ environment/service/env.go | 143 ++++++++++++++++++++++++++ environment/service/interface.go | 10 ++ environment/service/mock_interface.go | 57 ++++++++++ environment/service/models.go | 11 ++ go.mod | 18 +++- go.sum | 34 ++++++ main.go | 8 ++ 10 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 environment/handler/env.go create mode 100644 environment/handler/interface.go create mode 100644 environment/service/app_selector.go create mode 100644 environment/service/env.go create mode 100644 environment/service/interface.go create mode 100644 environment/service/mock_interface.go create mode 100644 environment/service/models.go diff --git a/environment/handler/env.go b/environment/handler/env.go new file mode 100644 index 0000000..da74b11 --- /dev/null +++ b/environment/handler/env.go @@ -0,0 +1,24 @@ +package handler + +import ( + "fmt" + + "gofr.dev/pkg/gofr" +) + +type Handler struct { + envSvc EnvAdder +} + +func New(envSvc EnvAdder) *Handler { + return &Handler{envSvc: envSvc} +} + +func (h *Handler) AddEnvironment(ctx *gofr.Context) (any, error) { + n, err := h.envSvc.AddEnvironments(ctx) + if err != nil { + return nil, err + } + + return fmt.Sprintf("%d enviromnets added", n), nil +} diff --git a/environment/handler/interface.go b/environment/handler/interface.go new file mode 100644 index 0000000..11c4031 --- /dev/null +++ b/environment/handler/interface.go @@ -0,0 +1,7 @@ +package handler + +import "gofr.dev/pkg/gofr" + +type EnvAdder interface { + AddEnvironments(ctx *gofr.Context) (int, error) +} diff --git a/environment/service/app_selector.go b/environment/service/app_selector.go new file mode 100644 index 0000000..8d208f9 --- /dev/null +++ b/environment/service/app_selector.go @@ -0,0 +1,102 @@ +package service + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const ( + listWidth = 20 + listHeight = 14 + listPaddingLeft = 2 + paginationPadding = 4 +) + +//nolint:gochecknoglobals //required TUI styles for displaying the list +var ( + itemStyle = lipgloss.NewStyle().PaddingLeft(listPaddingLeft) + selectedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("170")) + paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(paginationPadding) + helpStyle = list.DefaultStyles().HelpStyle +) + +type item struct { + id int + name string +} + +func (i *item) FilterValue() string { return i.name } + +type itemDelegate struct{} + +func (itemDelegate) Height() int { return 1 } +func (itemDelegate) Spacing() int { return 0 } +func (itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } + +// Render renders the list items with the selected item highlighted. +// +//nolint:gocritic //required to render the list items. +func (itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(*item) + if !ok { + return + } + + str := fmt.Sprintf("%3d. %s", index+1, i.name) + + fn := itemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return selectedItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + +type model struct { + choice *item + quitting bool + list list.Model +} + +func (*model) Init() tea.Cmd { + return nil +} + +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.list.SetWidth(msg.Width) + return m, nil + + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case "q", "ctrl+c": + m.quitting = true + return m, tea.Quit + + case "enter": + i, ok := m.list.SelectedItem().(*item) + if ok { + m.choice = i + } + + return m, tea.Quit + } + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + + return m, cmd +} + +func (m *model) View() string { + return "\n" + m.list.View() +} diff --git a/environment/service/env.go b/environment/service/env.go new file mode 100644 index 0000000..eec76ea --- /dev/null +++ b/environment/service/env.go @@ -0,0 +1,143 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "gofr.dev/pkg/gofr" +) + +var ( + ErrUnableToRenderApps = errors.New("unable to render the list of applications") + ErrConnectingZopAPI = errors.New("unable to connect to Zop API") + ErrorAddingEnv = errors.New("unable to add environment") + ErrNoApplicationSelected = errors.New("no application selected") +) + +type Service struct { + appGet ApplicationGetter +} + +func New(appGet ApplicationGetter) *Service { return &Service{appGet: appGet} } + +func (s *Service) AddEnvironments(ctx *gofr.Context) (int, error) { + app, err := s.getSelectedApplication(ctx) + if err != nil { + return 0, err + } + + ctx.Out.Println("Selected application: ", app.name) + ctx.Out.Println("Please provide names of environment to be added...") + + var ( + input string + level = 1 + ) + + for { + ctx.Out.Print("Enter environment name: ") + + _, _ = fmt.Scanf("%s", &input) + + err = postEnvironment(ctx, &Environment{Name: input, Level: level, ApplicationID: int64(app.id)}) + if err != nil { + return level, err + } + + level++ + + ctx.Out.Print("Do you wish to add more? (y/n) ") + + _, _ = fmt.Scanf("%s", &input) + + if input == "n" { + break + } + } + + return level, nil +} + +func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { + apps, err := s.appGet.GetApplications(ctx) + if err != nil { + return nil, err + } + + items := make([]list.Item, 0) + + for _, app := range apps { + items = append(items, &item{app.ID, app.Name}) + } + + l := list.New(items, itemDelegate{}, listWidth, listHeight) + l.Title = "Select the application where you want to add the environment!" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(true) + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle + l.SetShowStatusBar(false) + + m := model{list: l} + + if _, er := tea.NewProgram(&m).Run(); er != nil { + ctx.Logger.Errorf("unable to render the list of applications! %v", er) + + return nil, ErrUnableToRenderApps + } + + if m.choice == nil { + return nil, ErrNoApplicationSelected + } + + return m.choice, nil +} + +func postEnvironment(ctx *gofr.Context, env *Environment) error { + body, _ := json.Marshal(env) + + resp, err := ctx.GetHTTPService("api-service"). + PostWithHeaders(ctx, "environments", nil, body, map[string]string{ + "Content-Type": "application/json", + }) + if err != nil { + ctx.Logger.Errorf("unable to connect to Zop API! %v", err) + + return ErrConnectingZopAPI + } + + if resp.StatusCode != http.StatusCreated { + var errResp struct { + Errors any `json:"errors,omitempty"` + } + + err = getResponse(resp, &errResp) + if err != nil { + ctx.Logger.Errorf("unable to add environment!, could not decode error message %v", err) + } + + ctx.Logger.Errorf("unable to add environment! %v", resp) + + return ErrorAddingEnv + } + + return nil +} + +func getResponse(resp *http.Response, i any) error { + defer resp.Body.Close() + + b, _ := io.ReadAll(resp.Body) + + err := json.Unmarshal(b, i) + if err != nil { + return err + } + + return nil +} diff --git a/environment/service/interface.go b/environment/service/interface.go new file mode 100644 index 0000000..6bb730b --- /dev/null +++ b/environment/service/interface.go @@ -0,0 +1,10 @@ +package service + +import ( + "gofr.dev/pkg/gofr" + appSvc "zop.dev/cli/zop/application/service" +) + +type ApplicationGetter interface { + GetApplications(ctx *gofr.Context) ([]appSvc.Application, error) +} diff --git a/environment/service/mock_interface.go b/environment/service/mock_interface.go new file mode 100644 index 0000000..9fd52b4 --- /dev/null +++ b/environment/service/mock_interface.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -source=interface.go -destination=mock_interface.go -package=service +// + +// Package service is a generated GoMock package. +package service + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + gofr "gofr.dev/pkg/gofr" + service "zop.dev/cli/zop/application/service" +) + +// MockApplicationGetter is a mock of ApplicationGetter interface. +type MockApplicationGetter struct { + ctrl *gomock.Controller + recorder *MockApplicationGetterMockRecorder + isgomock struct{} +} + +// MockApplicationGetterMockRecorder is the mock recorder for MockApplicationGetter. +type MockApplicationGetterMockRecorder struct { + mock *MockApplicationGetter +} + +// NewMockApplicationGetter creates a new mock instance. +func NewMockApplicationGetter(ctrl *gomock.Controller) *MockApplicationGetter { + mock := &MockApplicationGetter{ctrl: ctrl} + mock.recorder = &MockApplicationGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockApplicationGetter) EXPECT() *MockApplicationGetterMockRecorder { + return m.recorder +} + +// GetApplications mocks base method. +func (m *MockApplicationGetter) GetApplications(ctx *gofr.Context) ([]service.Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetApplications", ctx) + ret0, _ := ret[0].([]service.Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetApplications indicates an expected call of GetApplications. +func (mr *MockApplicationGetterMockRecorder) GetApplications(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplications", reflect.TypeOf((*MockApplicationGetter)(nil).GetApplications), ctx) +} diff --git a/environment/service/models.go b/environment/service/models.go new file mode 100644 index 0000000..6e3edef --- /dev/null +++ b/environment/service/models.go @@ -0,0 +1,11 @@ +package service + +type Environment struct { + ID int64 `json:"id"` + ApplicationID int64 `json:"ApplicationID"` + Level int `json:"Level"` + Name string `json:"Name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + DeletedAt string `json:"deletedAt,omitempty"` +} diff --git a/go.mod b/go.mod index 179cbf8..31668c7 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,12 @@ module zop.dev/cli/zop go 1.22.8 require ( + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.2.4 + github.com/charmbracelet/lipgloss v1.0.0 github.com/mattn/go-sqlite3 v1.14.24 github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 gofr.dev v1.28.0 golang.org/x/oauth2 v0.24.0 @@ -21,13 +25,18 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect github.com/XSAM/otelsql v0.34.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/x/ansi v0.4.5 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/eclipse/paho.mqtt.golang v1.5.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -47,7 +56,13 @@ require ( github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect @@ -61,8 +76,9 @@ require ( github.com/redis/go-redis/extra/redisotel/v9 v9.7.0 // indirect github.com/redis/go-redis/v9 v9.7.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/segmentio/kafka-go v0.4.47 // indirect - github.com/stretchr/testify v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect diff --git a/go.sum b/go.sum index 4a0283a..46bc929 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,10 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZp github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -38,6 +42,16 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -53,6 +67,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -135,10 +151,22 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -171,8 +199,13 @@ github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0= github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -295,6 +328,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index 010aa3a..6ba77ac 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,8 @@ import ( impService "zop.dev/cli/zop/cloud/service/gcp" listSvc "zop.dev/cli/zop/cloud/service/list" impStore "zop.dev/cli/zop/cloud/store/gcp" + envHandler "zop.dev/cli/zop/environment/handler" + envService "zop.dev/cli/zop/environment/service" ) const ( @@ -58,5 +60,11 @@ func main() { app.SubCommand("application add", appH.Add) app.SubCommand("application list", appH.List) + // Environment + envSvc := envService.New(appSvc) + envH := envHandler.New(envSvc) + + app.SubCommand("environment add", envH.AddEnvironment) + app.Run() } From 3b195d18fcb16b9e4c4434b5854cb9b69399a0e6 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Mon, 16 Dec 2024 12:11:24 +0530 Subject: [PATCH 08/26] fix linter errors --- application/handler/application.go | 3 ++- application/handler/handler_test.go | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/application/handler/application.go b/application/handler/application.go index c56764f..3d578a8 100644 --- a/application/handler/application.go +++ b/application/handler/application.go @@ -3,11 +3,11 @@ package handler import ( "errors" "fmt" - "gofr.dev/pkg/gofr/cmd/terminal" "sort" "strings" "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/cmd/terminal" ) var ( @@ -47,6 +47,7 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { ctx.Out.Println("Applications and their environments:\n") s := strings.Builder{} + for i, app := range apps { ctx.Out.Printf("%d.", i+1) ctx.Out.SetColor(terminal.Cyan) diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go index a6bab64..b417817 100644 --- a/application/handler/handler_test.go +++ b/application/handler/handler_test.go @@ -2,16 +2,17 @@ package handler import ( "errors" - "gofr.dev/pkg/gofr/cmd/terminal" - "gofr.dev/pkg/gofr/testutil" "testing" - svc "zop.dev/cli/zop/application/service" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/cmd" + "gofr.dev/pkg/gofr/cmd/terminal" "gofr.dev/pkg/gofr/container" + "gofr.dev/pkg/gofr/testutil" + + svc "zop.dev/cli/zop/application/service" ) var errAPICall = errors.New("error in API call") @@ -89,10 +90,10 @@ func TestHandler_List(t *testing.T) { mockCalls: []*gomock.Call{ mockSvc.EXPECT().GetApplications(gomock.Any()). Return([]svc.Application{ - {1, "app1", - []svc.Environment{{"env1", 1, nil}, {"env2", 2, nil}}}, - {2, "app2", - []svc.Environment{{"dev", 1, nil}, {"prod", 2, nil}}}, + {ID: 1, Name: "app1", + Envs: []svc.Environment{{Name: "env1", Order: 1}, {Name: "env2", Order: 2}}}, + {ID: 2, Name: "app2", + Envs: []svc.Environment{{Name: "dev", Order: 1}, {Name: "prod", Order: 2}}}, }, nil), }, expected: "Applications and their environments:\n\n1.\x1b[38;5;6m app1 " + @@ -112,7 +113,6 @@ func TestHandler_List(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - h := New(mockSvc) out := testutil.StdoutOutputForFunc(func() { ctx := &gofr.Context{ From cacd5d79cd13ced377bb4c1261e8f6179d59e30c Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Mon, 16 Dec 2024 13:11:10 +0530 Subject: [PATCH 09/26] list all environments for an application --- environment/handler/env.go | 25 +++++++++++++++++--- environment/handler/interface.go | 11 ++++++--- environment/service/env.go | 39 ++++++++++++++++++++++++++++---- main.go | 1 + 4 files changed, 65 insertions(+), 11 deletions(-) diff --git a/environment/handler/env.go b/environment/handler/env.go index da74b11..891ba43 100644 --- a/environment/handler/env.go +++ b/environment/handler/env.go @@ -2,23 +2,42 @@ package handler import ( "fmt" + "sort" "gofr.dev/pkg/gofr" ) type Handler struct { - envSvc EnvAdder + envSvc EnvironmentService } -func New(envSvc EnvAdder) *Handler { +func New(envSvc EnvironmentService) *Handler { return &Handler{envSvc: envSvc} } func (h *Handler) AddEnvironment(ctx *gofr.Context) (any, error) { - n, err := h.envSvc.AddEnvironments(ctx) + n, err := h.envSvc.Add(ctx) if err != nil { return nil, err } return fmt.Sprintf("%d enviromnets added", n), nil } + +func (h *Handler) List(ctx *gofr.Context) (any, error) { + envs, err := h.envSvc.List(ctx) + if err != nil { + return nil, err + } + + sort.Slice(envs, func(i, j int) bool { return envs[i].ID < envs[j].ID }) + + // Print a table of all the environments in the application + ctx.Out.Println("ID\tName") + + for _, env := range envs { + ctx.Out.Printf("%d\t%s\n", env.ID, env.Name) + } + + return nil, nil +} diff --git a/environment/handler/interface.go b/environment/handler/interface.go index 11c4031..58762e7 100644 --- a/environment/handler/interface.go +++ b/environment/handler/interface.go @@ -1,7 +1,12 @@ package handler -import "gofr.dev/pkg/gofr" +import ( + "gofr.dev/pkg/gofr" -type EnvAdder interface { - AddEnvironments(ctx *gofr.Context) (int, error) + "zop.dev/cli/zop/environment/service" +) + +type EnvironmentService interface { + Add(ctx *gofr.Context) (int, error) + List(ctx *gofr.Context) ([]service.Environment, error) } diff --git a/environment/service/env.go b/environment/service/env.go index eec76ea..ff6109a 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -13,10 +13,11 @@ import ( ) var ( - ErrUnableToRenderApps = errors.New("unable to render the list of applications") - ErrConnectingZopAPI = errors.New("unable to connect to Zop API") - ErrorAddingEnv = errors.New("unable to add environment") - ErrNoApplicationSelected = errors.New("no application selected") + ErrUnableToRenderApps = errors.New("unable to render the list of applications") + ErrConnectingZopAPI = errors.New("unable to connect to Zop API") + ErrorAddingEnv = errors.New("unable to add environment") + ErrNoApplicationSelected = errors.New("no application selected") + ErrorFetchingEnvironments = errors.New("unable to fetch environments") ) type Service struct { @@ -25,7 +26,7 @@ type Service struct { func New(appGet ApplicationGetter) *Service { return &Service{appGet: appGet} } -func (s *Service) AddEnvironments(ctx *gofr.Context) (int, error) { +func (s *Service) Add(ctx *gofr.Context) (int, error) { app, err := s.getSelectedApplication(ctx) if err != nil { return 0, err @@ -63,6 +64,34 @@ func (s *Service) AddEnvironments(ctx *gofr.Context) (int, error) { return level, nil } +func (s *Service) List(ctx *gofr.Context) ([]Environment, error) { + app, err := s.getSelectedApplication(ctx) + if err != nil { + return nil, err + } + + resp, err := ctx.GetHTTPService("api-service"). + Get(ctx, fmt.Sprintf("applications/%d/environments", app.id), nil) + if err != nil { + ctx.Logger.Errorf("unable to connect to Zop API! %v", err) + + return nil, ErrConnectingZopAPI + } + + var data struct { + Envs []Environment `json:"data"` + } + + err = getResponse(resp, &data) + if err != nil { + ctx.Logger.Errorf("unable to fetch environments, could not unmarshall response %v", err) + + return nil, ErrorFetchingEnvironments + } + + return data.Envs, nil +} + func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { apps, err := s.appGet.GetApplications(ctx) if err != nil { diff --git a/main.go b/main.go index 6ba77ac..f7e0e87 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,7 @@ func main() { envH := envHandler.New(envSvc) app.SubCommand("environment add", envH.AddEnvironment) + app.SubCommand("environment list", envH.List) app.Run() } From f340f100be1bea65c4339aea25bf4ce965eda614 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Mon, 16 Dec 2024 12:11:24 +0530 Subject: [PATCH 10/26] fix linter errors --- application/handler/application.go | 3 ++- application/handler/handler_test.go | 16 ++++++++-------- environment/handler/env.go | 4 ++-- environment/handler/interface.go | 2 +- environment/service/env.go | 2 +- main.go | 2 +- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/application/handler/application.go b/application/handler/application.go index c56764f..3d578a8 100644 --- a/application/handler/application.go +++ b/application/handler/application.go @@ -3,11 +3,11 @@ package handler import ( "errors" "fmt" - "gofr.dev/pkg/gofr/cmd/terminal" "sort" "strings" "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/cmd/terminal" ) var ( @@ -47,6 +47,7 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { ctx.Out.Println("Applications and their environments:\n") s := strings.Builder{} + for i, app := range apps { ctx.Out.Printf("%d.", i+1) ctx.Out.SetColor(terminal.Cyan) diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go index a6bab64..b417817 100644 --- a/application/handler/handler_test.go +++ b/application/handler/handler_test.go @@ -2,16 +2,17 @@ package handler import ( "errors" - "gofr.dev/pkg/gofr/cmd/terminal" - "gofr.dev/pkg/gofr/testutil" "testing" - svc "zop.dev/cli/zop/application/service" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/cmd" + "gofr.dev/pkg/gofr/cmd/terminal" "gofr.dev/pkg/gofr/container" + "gofr.dev/pkg/gofr/testutil" + + svc "zop.dev/cli/zop/application/service" ) var errAPICall = errors.New("error in API call") @@ -89,10 +90,10 @@ func TestHandler_List(t *testing.T) { mockCalls: []*gomock.Call{ mockSvc.EXPECT().GetApplications(gomock.Any()). Return([]svc.Application{ - {1, "app1", - []svc.Environment{{"env1", 1, nil}, {"env2", 2, nil}}}, - {2, "app2", - []svc.Environment{{"dev", 1, nil}, {"prod", 2, nil}}}, + {ID: 1, Name: "app1", + Envs: []svc.Environment{{Name: "env1", Order: 1}, {Name: "env2", Order: 2}}}, + {ID: 2, Name: "app2", + Envs: []svc.Environment{{Name: "dev", Order: 1}, {Name: "prod", Order: 2}}}, }, nil), }, expected: "Applications and their environments:\n\n1.\x1b[38;5;6m app1 " + @@ -112,7 +113,6 @@ func TestHandler_List(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - h := New(mockSvc) out := testutil.StdoutOutputForFunc(func() { ctx := &gofr.Context{ diff --git a/environment/handler/env.go b/environment/handler/env.go index da74b11..e554fe0 100644 --- a/environment/handler/env.go +++ b/environment/handler/env.go @@ -14,8 +14,8 @@ func New(envSvc EnvAdder) *Handler { return &Handler{envSvc: envSvc} } -func (h *Handler) AddEnvironment(ctx *gofr.Context) (any, error) { - n, err := h.envSvc.AddEnvironments(ctx) +func (h *Handler) Add(ctx *gofr.Context) (any, error) { + n, err := h.envSvc.Add(ctx) if err != nil { return nil, err } diff --git a/environment/handler/interface.go b/environment/handler/interface.go index 11c4031..bb958c5 100644 --- a/environment/handler/interface.go +++ b/environment/handler/interface.go @@ -3,5 +3,5 @@ package handler import "gofr.dev/pkg/gofr" type EnvAdder interface { - AddEnvironments(ctx *gofr.Context) (int, error) + Add(ctx *gofr.Context) (int, error) } diff --git a/environment/service/env.go b/environment/service/env.go index eec76ea..3da11ff 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -25,7 +25,7 @@ type Service struct { func New(appGet ApplicationGetter) *Service { return &Service{appGet: appGet} } -func (s *Service) AddEnvironments(ctx *gofr.Context) (int, error) { +func (s *Service) Add(ctx *gofr.Context) (int, error) { app, err := s.getSelectedApplication(ctx) if err != nil { return 0, err diff --git a/main.go b/main.go index 6ba77ac..1d8b518 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,7 @@ func main() { envSvc := envService.New(appSvc) envH := envHandler.New(envSvc) - app.SubCommand("environment add", envH.AddEnvironment) + app.SubCommand("environment add", envH.Add) app.Run() } From 27f51a5718b844ab5ddfc89b7a52dd868a76ec8c Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Mon, 16 Dec 2024 13:24:58 +0530 Subject: [PATCH 11/26] rename methods according to the convention --- application/handler/application.go | 5 +++-- application/handler/handler_test.go | 13 ++++++------- application/handler/interface.go | 4 ++-- application/handler/mock_interface.go | 12 ++++++------ application/service/application.go | 4 ++-- application/service/application_test.go | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/application/handler/application.go b/application/handler/application.go index d4afb09..4bc41e2 100644 --- a/application/handler/application.go +++ b/application/handler/application.go @@ -30,7 +30,7 @@ func (h *Handler) Add(ctx *gofr.Context) (any, error) { return nil, ErrorApplicationNameNotProvided } - err := h.appAdd.AddApplication(ctx, name) + err := h.appAdd.Add(ctx, name) if err != nil { return nil, err } @@ -39,7 +39,7 @@ func (h *Handler) Add(ctx *gofr.Context) (any, error) { } func (h *Handler) List(ctx *gofr.Context) (any, error) { - apps, err := h.appAdd.GetApplications(ctx) + apps, err := h.appAdd.List(ctx) if err != nil { return nil, err } @@ -47,6 +47,7 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { ctx.Out.Println("Applications and their environments:\n") s := strings.Builder{} + for i, app := range apps { ctx.Out.Printf("%d.", i+1) ctx.Out.SetColor(terminal.Cyan) diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go index 39f10a1..b417817 100644 --- a/application/handler/handler_test.go +++ b/application/handler/handler_test.go @@ -5,12 +5,12 @@ import ( "testing" "github.com/stretchr/testify/require" - "gofr.dev/pkg/gofr/cmd/terminal" - "gofr.dev/pkg/gofr/testutil" "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/cmd" + "gofr.dev/pkg/gofr/cmd/terminal" "gofr.dev/pkg/gofr/container" + "gofr.dev/pkg/gofr/testutil" svc "zop.dev/cli/zop/application/service" ) @@ -90,10 +90,10 @@ func TestHandler_List(t *testing.T) { mockCalls: []*gomock.Call{ mockSvc.EXPECT().GetApplications(gomock.Any()). Return([]svc.Application{ - {1, "app1", - []svc.Environment{{"env1", 1, nil}, {"env2", 2, nil}}}, - {2, "app2", - []svc.Environment{{"dev", 1, nil}, {"prod", 2, nil}}}, + {ID: 1, Name: "app1", + Envs: []svc.Environment{{Name: "env1", Order: 1}, {Name: "env2", Order: 2}}}, + {ID: 2, Name: "app2", + Envs: []svc.Environment{{Name: "dev", Order: 1}, {Name: "prod", Order: 2}}}, }, nil), }, expected: "Applications and their environments:\n\n1.\x1b[38;5;6m app1 " + @@ -113,7 +113,6 @@ func TestHandler_List(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - h := New(mockSvc) out := testutil.StdoutOutputForFunc(func() { ctx := &gofr.Context{ diff --git a/application/handler/interface.go b/application/handler/interface.go index 6d91953..d90b4d3 100644 --- a/application/handler/interface.go +++ b/application/handler/interface.go @@ -7,6 +7,6 @@ import ( ) type ApplicationService interface { - AddApplication(ctx *gofr.Context, name string) error - GetApplications(ctx *gofr.Context) ([]service.Application, error) + Add(ctx *gofr.Context, name string) error + List(ctx *gofr.Context) ([]service.Application, error) } diff --git a/application/handler/mock_interface.go b/application/handler/mock_interface.go index 7340b69..b5693a3 100644 --- a/application/handler/mock_interface.go +++ b/application/handler/mock_interface.go @@ -42,9 +42,9 @@ func (m *MockApplicationService) EXPECT() *MockApplicationServiceMockRecorder { } // AddApplication mocks base method. -func (m *MockApplicationService) AddApplication(ctx *gofr.Context, name string) error { +func (m *MockApplicationService) Add(ctx *gofr.Context, name string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddApplication", ctx, name) + ret := m.ctrl.Call(m, "Add", ctx, name) ret0, _ := ret[0].(error) return ret0 } @@ -52,13 +52,13 @@ func (m *MockApplicationService) AddApplication(ctx *gofr.Context, name string) // AddApplication indicates an expected call of AddApplication. func (mr *MockApplicationServiceMockRecorder) AddApplication(ctx, name any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddApplication", reflect.TypeOf((*MockApplicationService)(nil).AddApplication), ctx, name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockApplicationService)(nil).Add), ctx, name) } // GetApplications mocks base method. -func (m *MockApplicationService) GetApplications(ctx *gofr.Context) ([]service.Application, error) { +func (m *MockApplicationService) List(ctx *gofr.Context) ([]service.Application, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetApplications", ctx) + ret := m.ctrl.Call(m, "List", ctx) ret0, _ := ret[0].([]service.Application) ret1, _ := ret[1].(error) return ret0, ret1 @@ -67,5 +67,5 @@ func (m *MockApplicationService) GetApplications(ctx *gofr.Context) ([]service.A // GetApplications indicates an expected call of GetApplications. func (mr *MockApplicationServiceMockRecorder) GetApplications(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplications", reflect.TypeOf((*MockApplicationService)(nil).GetApplications), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockApplicationService)(nil).List), ctx) } diff --git a/application/service/application.go b/application/service/application.go index 445354d..32eb348 100644 --- a/application/service/application.go +++ b/application/service/application.go @@ -16,7 +16,7 @@ func New() *Service { return &Service{} } -func (*Service) AddApplication(ctx *gofr.Context, name string) error { +func (*Service) Add(ctx *gofr.Context, name string) error { var ( envs []Environment input string @@ -69,7 +69,7 @@ func (*Service) AddApplication(ctx *gofr.Context, name string) error { return nil } -func (*Service) GetApplications(ctx *gofr.Context) ([]Application, error) { +func (*Service) List(ctx *gofr.Context) ([]Application, error) { api := ctx.GetHTTPService("api-service") reps, err := api.Get(ctx, "applications", nil) diff --git a/application/service/application_test.go b/application/service/application_test.go index 70c46ec..cc71beb 100644 --- a/application/service/application_test.go +++ b/application/service/application_test.go @@ -69,7 +69,7 @@ func Test_AddApplication(t *testing.T) { t.Run(tt.name, func(t *testing.T) { s := New() - errSvc := s.AddApplication(ctx, "test") + errSvc := s.Add(ctx, "test") require.Equal(t, tt.expError, errSvc) }) @@ -147,7 +147,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { defer func() { os.Stdin = oldStdin }() - errSvc := s.AddApplication(ctx, "test") + errSvc := s.Add(ctx, "test") require.Equal(t, tt.expError, errSvc) }) } From 40ffbb717f474ed68a9b7551ef654c75a85d9fe9 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Mon, 16 Dec 2024 13:55:47 +0530 Subject: [PATCH 12/26] add test for list function --- application/service/application.go | 5 +- application/service/application_test.go | 87 ++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/application/service/application.go b/application/service/application.go index 32eb348..05dae1d 100644 --- a/application/service/application.go +++ b/application/service/application.go @@ -86,7 +86,10 @@ func (*Service) List(ctx *gofr.Context) ([]Application, error) { err = json.Unmarshal(body, &apps) if err != nil { - return nil, err + return nil, &ErrAPIService{ + StatusCode: http.StatusInternalServerError, + Message: "Internal Server Error", + } } return apps.Data, nil diff --git a/application/service/application_test.go b/application/service/application_test.go index cc71beb..9834422 100644 --- a/application/service/application_test.go +++ b/application/service/application_test.go @@ -19,7 +19,7 @@ import ( var errAPICall = errors.New("error in API call") -func Test_AddApplication(t *testing.T) { +func Test_Add(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -36,29 +36,33 @@ func Test_AddApplication(t *testing.T) { testCases := []struct { name string + input string mockCalls []*gomock.Call expError error }{ { - name: "success Post call", + name: "success Post call", + input: "n\n", mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). Return(&http.Response{StatusCode: http.StatusCreated, Body: io.NopCloser(&errorReader{})}, nil), }, expError: nil, }, { - name: "error in Post call", + name: "error in Post call", + input: "n\n", mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). Return(nil, errAPICall), }, expError: errAPICall, }, { - name: "unexpected response", + name: "unexpected response", + input: "n\n", mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). Return(&http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(bytes.NewBuffer(b))}, nil), }, expError: &ErrAPIService{StatusCode: http.StatusInternalServerError, Message: "Something went wrong"}, @@ -69,14 +73,21 @@ func Test_AddApplication(t *testing.T) { t.Run(tt.name, func(t *testing.T) { s := New() + r, w, _ := os.Pipe() + os.Stdin = r + _, _ = w.WriteString(tt.input) + errSvc := s.Add(ctx, "test") require.Equal(t, tt.expError, errSvc) + + r.Close() + w.Close() }) } } -func Test_AddApplication_WithEnvs(t *testing.T) { +func Test_Add_WithEnvs(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -102,7 +113,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { {Name: "dev", Order: 2}, }, mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). DoAndReturn(func(_ *gofr.Context, _ string, _, body, _ interface{}) (*http.Response, error) { var app Application _ = json.Unmarshal(body.([]byte), &app) @@ -121,7 +132,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { userInput: "n\n", expectedEnvs: []Environment{}, mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). DoAndReturn(func(_ *gofr.Context, _ string, _, body, _ interface{}) (*http.Response, error) { var app Application _ = json.Unmarshal(body.([]byte), &app) @@ -152,3 +163,59 @@ func Test_AddApplication_WithEnvs(t *testing.T) { }) } } + +func Test_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCont, mocks := container.NewMockContainer(t, func(_ *container.Container, ctrl *gomock.Controller) any { + return service.NewMockHTTP(ctrl) + }) + + mockCont.Services["api-service"] = mocks.HTTPService + ctx := &gofr.Context{Container: mockCont, Out: terminal.New()} + + testCases := []struct { + name string + mockCalls []*gomock.Call + expError error + }{ + { + name: "success Get call", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().Get(ctx, "applications", nil). + Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(`{ "data" : null }`))}, nil), + }, + expError: nil, + }, + { + name: "error in Get call", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().Get(ctx, "applications", nil). + Return(nil, errAPICall), + }, + expError: errAPICall, + }, + { + name: "unexpected response", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().Get(ctx, "applications", nil). + Return(&http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(bytes.NewBuffer(nil))}, nil), + }, + expError: &ErrAPIService{StatusCode: http.StatusInternalServerError, Message: "Internal Server Error"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + s := New() + + apps, errSvc := s.List(ctx) + require.Equal(t, tt.expError, errSvc) + + if tt.expError == nil { + require.Empty(t, apps) + } + }) + } +} From dec59de5219e722d39e13d37295601da25904f26 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Mon, 16 Dec 2024 14:03:53 +0530 Subject: [PATCH 13/26] refactor application lister interface --- environment/service/env.go | 2 +- environment/service/interface.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment/service/env.go b/environment/service/env.go index 3da11ff..08436a5 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -64,7 +64,7 @@ func (s *Service) Add(ctx *gofr.Context) (int, error) { } func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { - apps, err := s.appGet.GetApplications(ctx) + apps, err := s.appGet.List(ctx) if err != nil { return nil, err } diff --git a/environment/service/interface.go b/environment/service/interface.go index 6bb730b..a170e0d 100644 --- a/environment/service/interface.go +++ b/environment/service/interface.go @@ -6,5 +6,5 @@ import ( ) type ApplicationGetter interface { - GetApplications(ctx *gofr.Context) ([]appSvc.Application, error) + List(ctx *gofr.Context) ([]appSvc.Application, error) } From f65bf7274f6610a830e88211dd316bacc5a1e6e9 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Mon, 16 Dec 2024 17:56:25 +0530 Subject: [PATCH 14/26] add godoc for environment implementation --- environment/handler/env.go | 14 +++++- environment/service/app_selector.go | 67 ++++++++++++++++++++--------- environment/service/env.go | 36 +++++++++++----- environment/service/interface.go | 4 ++ environment/service/models.go | 30 ++++++++++--- 5 files changed, 113 insertions(+), 38 deletions(-) diff --git a/environment/handler/env.go b/environment/handler/env.go index e554fe0..0b2af11 100644 --- a/environment/handler/env.go +++ b/environment/handler/env.go @@ -1,3 +1,4 @@ +// Package handler provides the CMD handler logic for managing environments. package handler import ( @@ -6,19 +7,30 @@ import ( "gofr.dev/pkg/gofr" ) +// Handler is responsible for managing environment-related operations. type Handler struct { envSvc EnvAdder } +// New creates a new Handler with the given EnvAdder service. func New(envSvc EnvAdder) *Handler { return &Handler{envSvc: envSvc} } +// Add handles the HTTP request to add environments. It delegates the task +// to the EnvAdder service and returns a success message or an error. +// +// Parameters: +// - ctx: The GoFR context containing request data. +// +// Returns: +// - A success message indicating how many environments were added, or an error +// if the operation failed. func (h *Handler) Add(ctx *gofr.Context) (any, error) { n, err := h.envSvc.Add(ctx) if err != nil { return nil, err } - return fmt.Sprintf("%d enviromnets added", n), nil + return fmt.Sprintf("%d environments added", n), nil } diff --git a/environment/service/app_selector.go b/environment/service/app_selector.go index 8d208f9..b050887 100644 --- a/environment/service/app_selector.go +++ b/environment/service/app_selector.go @@ -1,3 +1,7 @@ +// Package service provides functionalities for interacting with applications and environments. +// It supports selecting an application and adding environments to it by communicating with an external API. +// it gives users a text-based user interface (TUI) for displaying and selecting items +// using the Charmbracelet bubbletea and list packages. package service import ( @@ -11,36 +15,55 @@ import ( ) const ( - listWidth = 20 - listHeight = 14 - listPaddingLeft = 2 + // listWidth defines the width of the list. + listWidth = 20 + // listHeight defines the height of the list. + listHeight = 14 + // listPaddingLeft defines the left padding of the list items. + listPaddingLeft = 2 + // paginationPadding defines the padding for pagination controls. paginationPadding = 4 ) //nolint:gochecknoglobals //required TUI styles for displaying the list var ( - itemStyle = lipgloss.NewStyle().PaddingLeft(listPaddingLeft) + // itemStyle defines the default style for list items. + itemStyle = lipgloss.NewStyle().PaddingLeft(listPaddingLeft) + // selectedItemStyle defines the style for the selected list item. selectedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("170")) - paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(paginationPadding) - helpStyle = list.DefaultStyles().HelpStyle + // paginationStyle defines the style for pagination controls. + paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(paginationPadding) + // helpStyle defines the style for the help text. + helpStyle = list.DefaultStyles().HelpStyle ) +// item represents a single item in the list. type item struct { - id int - name string + id int // ID is the unique identifier for the item. + name string // Name is the display name of the item. } -func (i *item) FilterValue() string { return i.name } +// FilterValue returns the value to be used for filtering list items. +// In this case, it's the name of the item. +func (i *item) FilterValue() string { + return i.name +} +// itemDelegate is a struct responsible for rendering and interacting with list items. type itemDelegate struct{} -func (itemDelegate) Height() int { return 1 } -func (itemDelegate) Spacing() int { return 0 } -func (itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +// Height returns the height of the item (always 1). +func (itemDelegate) Height() int { return 1 } + +// Spacing returns the spacing between items (always 0). +func (itemDelegate) Spacing() int { return 0 } + +// Update is used to handle updates to the item model. It doesn't do anything in this case. +func (itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { + return nil +} -// Render renders the list items with the selected item highlighted. -// -//nolint:gocritic //required to render the list items. +// Render renders a single list item, applying the selected item style if it's the currently selected item. func (itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { i, ok := listItem.(*item) if !ok { @@ -59,16 +82,20 @@ func (itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.I fmt.Fprint(w, fn(str)) } +// model represents the state of the TUI interface, including the list and selected item. type model struct { - choice *item - quitting bool - list list.Model + choice *item // choice is the selected item. + quitting bool // quitting indicates if the application is quitting. + list list.Model // list holds the list of items displayed in the TUI. } +// Init initializes the model, returning nil for no commands. func (*model) Init() tea.Cmd { return nil } +// Update handles updates from messages, such as key presses or window resizing. +// It updates the list and handles quitting or selecting an item. func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: @@ -78,7 +105,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch keypress := msg.String(); keypress { case "q", "ctrl+c": - m.quitting = true + m.quitting = true // Set quitting to true when 'q' or 'ctrl+c' is pressed. return m, tea.Quit case "enter": @@ -86,7 +113,6 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if ok { m.choice = i } - return m, tea.Quit } } @@ -97,6 +123,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +// View renders the view of the current model, displaying the list to the user. func (m *model) View() string { return "\n" + m.list.View() } diff --git a/environment/service/env.go b/environment/service/env.go index 08436a5..e4edb6c 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -13,18 +13,28 @@ import ( ) var ( - ErrUnableToRenderApps = errors.New("unable to render the list of applications") - ErrConnectingZopAPI = errors.New("unable to connect to Zop API") - ErrorAddingEnv = errors.New("unable to add environment") + // ErrUnableToRenderApps is returned when the application list cannot be rendered. + ErrUnableToRenderApps = errors.New("unable to render the list of applications") + // ErrConnectingZopAPI is returned when there is an error connecting to the Zop API. + ErrConnectingZopAPI = errors.New("unable to connect to Zop API") + // ErrorAddingEnv is returned when there is an error adding an environment. + ErrorAddingEnv = errors.New("unable to add environment") + // ErrNoApplicationSelected is returned when no application is selected. ErrNoApplicationSelected = errors.New("no application selected") ) +// Service represents the application service that handles application and environment operations. type Service struct { - appGet ApplicationGetter + appGet ApplicationGetter // appGet is responsible for fetching the list of applications. } -func New(appGet ApplicationGetter) *Service { return &Service{appGet: appGet} } +// New creates a new Service instance with the provided ApplicationGetter. +func New(appGet ApplicationGetter) *Service { + return &Service{appGet: appGet} +} +// Add prompts the user to add environments to a selected application. +// It returns the number of environments added and an error, if any. func (s *Service) Add(ctx *gofr.Context) (int, error) { app, err := s.getSelectedApplication(ctx) if err != nil { @@ -32,13 +42,14 @@ func (s *Service) Add(ctx *gofr.Context) (int, error) { } ctx.Out.Println("Selected application: ", app.name) - ctx.Out.Println("Please provide names of environment to be added...") + ctx.Out.Println("Please provide names of environments to be added...") var ( input string level = 1 ) + // Loop to gather environment names from the user and add them to the application. for { ctx.Out.Print("Enter environment name: ") @@ -51,6 +62,7 @@ func (s *Service) Add(ctx *gofr.Context) (int, error) { level++ + // Ask the user if they want to add more environments. ctx.Out.Print("Do you wish to add more? (y/n) ") _, _ = fmt.Scanf("%s", &input) @@ -63,18 +75,21 @@ func (s *Service) Add(ctx *gofr.Context) (int, error) { return level, nil } +// getSelectedApplication renders a list of applications for the user to select from. +// It returns the selected application or an error if no selection is made. func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { apps, err := s.appGet.List(ctx) if err != nil { return nil, err } + // Prepare a list of items for the user to select from. items := make([]list.Item, 0) - for _, app := range apps { items = append(items, &item{app.ID, app.Name}) } + // Initialize the list component for application selection. l := list.New(items, itemDelegate{}, listWidth, listHeight) l.Title = "Select the application where you want to add the environment!" l.SetShowStatusBar(false) @@ -85,9 +100,9 @@ func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { m := model{list: l} + // Render the list using the bubbletea program. if _, er := tea.NewProgram(&m).Run(); er != nil { ctx.Logger.Errorf("unable to render the list of applications! %v", er) - return nil, ErrUnableToRenderApps } @@ -98,6 +113,8 @@ func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { return m.choice, nil } +// postEnvironment sends a POST request to the API to add the provided environment to the application. +// It returns an error if the request fails or the response status code is not created (201). func postEnvironment(ctx *gofr.Context, env *Environment) error { body, _ := json.Marshal(env) @@ -107,7 +124,6 @@ func postEnvironment(ctx *gofr.Context, env *Environment) error { }) if err != nil { ctx.Logger.Errorf("unable to connect to Zop API! %v", err) - return ErrConnectingZopAPI } @@ -122,13 +138,13 @@ func postEnvironment(ctx *gofr.Context, env *Environment) error { } ctx.Logger.Errorf("unable to add environment! %v", resp) - return ErrorAddingEnv } return nil } +// getResponse reads the HTTP response body and unmarshals it into the provided interface. func getResponse(resp *http.Response, i any) error { defer resp.Body.Close() diff --git a/environment/service/interface.go b/environment/service/interface.go index a170e0d..aaf7d29 100644 --- a/environment/service/interface.go +++ b/environment/service/interface.go @@ -5,6 +5,10 @@ import ( appSvc "zop.dev/cli/zop/application/service" ) +// ApplicationGetter interface is used to abstract the process of fetching application data, +// which can be implemented by any service that has access to application-related data. type ApplicationGetter interface { + // List fetches a list of applications from the service. + // It returns a slice of Application objects and an error if the request fails. List(ctx *gofr.Context) ([]appSvc.Application, error) } diff --git a/environment/service/models.go b/environment/service/models.go index 6e3edef..ac4645c 100644 --- a/environment/service/models.go +++ b/environment/service/models.go @@ -1,11 +1,27 @@ package service +// Environment represents an environment within an application. +// It holds details about the environment, such as its ID, associated application ID, +// level, name, and timestamps for when it was created, updated, and optionally deleted. type Environment struct { - ID int64 `json:"id"` - ApplicationID int64 `json:"ApplicationID"` - Level int `json:"Level"` - Name string `json:"Name"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - DeletedAt string `json:"deletedAt,omitempty"` + // ID is the unique identifier of the environment. + ID int64 `json:"id"` + + // ApplicationID is the identifier of the application to which this environment belongs. + ApplicationID int64 `json:"ApplicationID"` + + // Level indicates the environment's level, which might be used to denote the hierarchy or order of environments. + Level int `json:"Level"` + + // Name is the name of the environment. + Name string `json:"Name"` + + // CreatedAt is the timestamp of when the environment was created. + CreatedAt string `json:"createdAt"` + + // UpdatedAt is the timestamp of when the environment was last updated. + UpdatedAt string `json:"updatedAt"` + + // DeletedAt is the timestamp of when the environment was deleted, if applicable. This field is optional. + DeletedAt string `json:"deletedAt,omitempty"` } From 03d74fd0da92400780718feb3dee7fdf5f6e50d2 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Tue, 17 Dec 2024 12:23:00 +0530 Subject: [PATCH 15/26] add table printing for the envs --- environment/handler/env.go | 18 ++++++++++++++++-- environment/service/env.go | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/environment/handler/env.go b/environment/handler/env.go index 87dda87..c26389d 100644 --- a/environment/handler/env.go +++ b/environment/handler/env.go @@ -2,7 +2,9 @@ package handler import ( "fmt" + "os" "sort" + "text/tabwriter" "gofr.dev/pkg/gofr" ) @@ -33,11 +35,23 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { sort.Slice(envs, func(i, j int) bool { return envs[i].ID < envs[j].ID }) // Print a table of all the environments in the application - ctx.Out.Println("ID\tName") + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug) + // Print table headers + fmt.Fprintln(writer, "ID\tApplicationID\tLevel\tName\tCreatedAt\tUpdatedAt") + + // Print rows for each environment for _, env := range envs { - ctx.Out.Printf("%d\t%s\n", env.ID, env.Name) + fmt.Fprintf(writer, "%s\t%d\t%s\t%s\n", + env.Name, + env.Level, + env.CreatedAt, + env.UpdatedAt, + ) } + // Flush the writer to output the table + writer.Flush() + return nil, nil } diff --git a/environment/service/env.go b/environment/service/env.go index ea7ecf3..7e5b3ab 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -114,7 +114,7 @@ func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { m := model{list: l} - if _, er := tea.NewProgram(&m).Run(); er != nil { + if _, er := tea.NewProgram(&m, tea.WithAltScreen()).Run(); er != nil { ctx.Logger.Errorf("unable to render the list of applications! %v", er) return nil, ErrUnableToRenderApps From 3e3ed00a1f041b59cba7e81892e9b54a45e500e2 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Tue, 17 Dec 2024 13:22:47 +0530 Subject: [PATCH 16/26] inital commit --- deploymentspace/handler/deployment.go | 14 +++++++ deploymentspace/handler/interface.go | 7 ++++ deploymentspace/service/deployment.go | 53 +++++++++++++++++++++++++++ deploymentspace/service/interface.go | 20 ++++++++++ 4 files changed, 94 insertions(+) create mode 100644 deploymentspace/handler/deployment.go create mode 100644 deploymentspace/handler/interface.go create mode 100644 deploymentspace/service/deployment.go create mode 100644 deploymentspace/service/interface.go diff --git a/deploymentspace/handler/deployment.go b/deploymentspace/handler/deployment.go new file mode 100644 index 0000000..a960ab8 --- /dev/null +++ b/deploymentspace/handler/deployment.go @@ -0,0 +1,14 @@ +package handler + +import "gofr.dev/pkg/gofr" + +type Handler struct{} + +func New() *Handler { + return &Handler{} +} + +func (h *Handler) Add(ctx *gofr.Context) (any, error) { + // Add your code here + return nil, nil +} diff --git a/deploymentspace/handler/interface.go b/deploymentspace/handler/interface.go new file mode 100644 index 0000000..2eb91d6 --- /dev/null +++ b/deploymentspace/handler/interface.go @@ -0,0 +1,7 @@ +package handler + +import "gofr.dev/pkg/gofr" + +type DeploymentService interface { + Add(ctx *gofr.Context) error +} diff --git a/deploymentspace/service/deployment.go b/deploymentspace/service/deployment.go new file mode 100644 index 0000000..0f9d6ff --- /dev/null +++ b/deploymentspace/service/deployment.go @@ -0,0 +1,53 @@ +package service + +import "gofr.dev/pkg/gofr" + +type Service struct { + cloudGet CloudAccountService + appGet ApplicationService +} + +func New() *Service { + return &Service{} +} + +func (*Service) Add(ctx *gofr.Context) error { + apiSvc := ctx.GetHTTPService("api-service") + + return nil +} + +func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { + apps, err := s.appGet.List(ctx) + if err != nil { + return nil, err + } + + items := make([]list.Item, 0) + + for _, app := range apps { + items = append(items, &item{app.ID, app.Name}) + } + + l := list.New(items, itemDelegate{}, listWidth, listHeight) + l.Title = "Select the application where you want to add the environment!" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(true) + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle + l.SetShowStatusBar(false) + + m := model{list: l} + + if _, er := tea.NewProgram(&m).Run(); er != nil { + ctx.Logger.Errorf("unable to render the list of applications! %v", er) + + return nil, ErrUnableToRenderApps + } + + if m.choice == nil { + return nil, ErrNoApplicationSelected + } + + return m.choice, nil +} diff --git a/deploymentspace/service/interface.go b/deploymentspace/service/interface.go new file mode 100644 index 0000000..d5b8901 --- /dev/null +++ b/deploymentspace/service/interface.go @@ -0,0 +1,20 @@ +package service + +import ( + "gofr.dev/pkg/gofr" + + appSvc "zop.dev/cli/zop/application/service" + cloudSvc "zop.dev/cli/zop/cloud/service/list" +) + +type CloudAccountService interface { + GetAccounts(ctx *gofr.Context) ([]*cloudSvc.CloudAccountResponse, error) +} + +type ApplicationService interface { + List(ctx *gofr.Context) ([]appSvc.Application, error) +} + +//type EnvironmentService interface { +// List(ctx *gofr.Context) ([]Environment, error) +//} From 63705c5988b4dde11b6575f41235a9d63860d579 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Tue, 17 Dec 2024 15:40:16 +0530 Subject: [PATCH 17/26] refactor list generation for recurring use --- environment/handler/env.go | 12 +++-- environment/service/env.go | 35 +++++--------- environment/service/models.go | 1 - .../service/app_selector.go => utils/list.go | 47 ++++++++++++++----- 4 files changed, 57 insertions(+), 38 deletions(-) rename environment/service/app_selector.go => utils/list.go (69%) diff --git a/environment/handler/env.go b/environment/handler/env.go index c26389d..edfb2bd 100644 --- a/environment/handler/env.go +++ b/environment/handler/env.go @@ -1,14 +1,16 @@ package handler import ( + "bytes" "fmt" - "os" "sort" "text/tabwriter" "gofr.dev/pkg/gofr" ) +const padding = 2 + type Handler struct { envSvc EnvironmentService } @@ -34,11 +36,13 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { sort.Slice(envs, func(i, j int) bool { return envs[i].ID < envs[j].ID }) + b := bytes.NewBuffer([]byte{}) + // Print a table of all the environments in the application - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug) + writer := tabwriter.NewWriter(b, 0, 0, padding, ' ', tabwriter.Debug) // Print table headers - fmt.Fprintln(writer, "ID\tApplicationID\tLevel\tName\tCreatedAt\tUpdatedAt") + fmt.Fprintln(writer, "Name\tLevel\tCreatedAt\tUpdatedAt") // Print rows for each environment for _, env := range envs { @@ -53,5 +57,5 @@ func (h *Handler) List(ctx *gofr.Context) (any, error) { // Flush the writer to output the table writer.Flush() - return nil, nil + return b.String(), nil } diff --git a/environment/service/env.go b/environment/service/env.go index 7e5b3ab..042371d 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -7,9 +7,9 @@ import ( "io" "net/http" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" "gofr.dev/pkg/gofr" + + "zop.dev/cli/zop/utils" ) var ( @@ -32,7 +32,7 @@ func (s *Service) Add(ctx *gofr.Context) (int, error) { return 0, err } - ctx.Out.Println("Selected application: ", app.name) + ctx.Out.Println("Selected application: ", app) ctx.Out.Println("Please provide names of environment to be added...") var ( @@ -45,7 +45,7 @@ func (s *Service) Add(ctx *gofr.Context) (int, error) { _, _ = fmt.Scanf("%s", &input) - err = postEnvironment(ctx, &Environment{Name: input, Level: level, ApplicationID: int64(app.id)}) + err = postEnvironment(ctx, &Environment{Name: input, Level: level, ApplicationID: int64(app.ID)}) if err != nil { return level, err } @@ -71,7 +71,7 @@ func (s *Service) List(ctx *gofr.Context) ([]Environment, error) { } resp, err := ctx.GetHTTPService("api-service"). - Get(ctx, fmt.Sprintf("applications/%d/environments", app.id), nil) + Get(ctx, fmt.Sprintf("applications/%d/environments", app.ID), nil) if err != nil { ctx.Logger.Errorf("unable to connect to Zop API! %v", err) @@ -92,39 +92,30 @@ func (s *Service) List(ctx *gofr.Context) ([]Environment, error) { return data.Envs, nil } -func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { +func (s *Service) getSelectedApplication(ctx *gofr.Context) (*utils.Item, error) { apps, err := s.appGet.List(ctx) if err != nil { return nil, err } - items := make([]list.Item, 0) + items := make([]*utils.Item, 0) for _, app := range apps { - items = append(items, &item{app.ID, app.Name}) + items = append(items, &utils.Item{ID: app.ID, Name: app.Name}) } - l := list.New(items, itemDelegate{}, listWidth, listHeight) - l.Title = "Select the application where you want to add the environment!" - l.SetShowStatusBar(false) - l.SetFilteringEnabled(true) - l.Styles.PaginationStyle = paginationStyle - l.Styles.HelpStyle = helpStyle - l.SetShowStatusBar(false) - - m := model{list: l} - - if _, er := tea.NewProgram(&m, tea.WithAltScreen()).Run(); er != nil { - ctx.Logger.Errorf("unable to render the list of applications! %v", er) + choice, err := utils.RenderList(items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of applications! %v", err) return nil, ErrUnableToRenderApps } - if m.choice == nil { + if choice == nil { return nil, ErrNoApplicationSelected } - return m.choice, nil + return choice, nil } func postEnvironment(ctx *gofr.Context, env *Environment) error { diff --git a/environment/service/models.go b/environment/service/models.go index 6e3edef..eadea8b 100644 --- a/environment/service/models.go +++ b/environment/service/models.go @@ -7,5 +7,4 @@ type Environment struct { Name string `json:"Name"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` - DeletedAt string `json:"deletedAt,omitempty"` } diff --git a/environment/service/app_selector.go b/utils/list.go similarity index 69% rename from environment/service/app_selector.go rename to utils/list.go index 8d208f9..4d8a6d7 100644 --- a/environment/service/app_selector.go +++ b/utils/list.go @@ -1,4 +1,4 @@ -package service +package utils import ( "fmt" @@ -11,10 +11,10 @@ import ( ) const ( - listWidth = 20 - listHeight = 14 listPaddingLeft = 2 paginationPadding = 4 + listWidth = 20 + listHeight = 14 ) //nolint:gochecknoglobals //required TUI styles for displaying the list @@ -25,12 +25,13 @@ var ( helpStyle = list.DefaultStyles().HelpStyle ) -type item struct { - id int - name string +type Item struct { + ID int + Name string + Data any } -func (i *item) FilterValue() string { return i.name } +func (i *Item) FilterValue() string { return i.Name } type itemDelegate struct{} @@ -42,12 +43,12 @@ func (itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } // //nolint:gocritic //required to render the list items. func (itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - i, ok := listItem.(*item) + i, ok := listItem.(*Item) if !ok { return } - str := fmt.Sprintf("%3d. %s", index+1, i.name) + str := fmt.Sprintf("%3d. %s", index+1, i.Name) fn := itemStyle.Render if index == m.Index() { @@ -60,7 +61,7 @@ func (itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.I } type model struct { - choice *item + choice *Item quitting bool list list.Model } @@ -82,7 +83,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit case "enter": - i, ok := m.list.SelectedItem().(*item) + i, ok := m.list.SelectedItem().(*Item) if ok { m.choice = i } @@ -100,3 +101,27 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *model) View() string { return "\n" + m.list.View() } + +func RenderList(items []*Item) (*Item, error) { + listItems := make([]list.Item, 0) + + for i := range items { + listItems = append(listItems, items[i]) + } + + l := list.New(listItems, itemDelegate{}, listWidth, listHeight) + l.Title = "Select the application where you want to add the environment!" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(true) + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle + l.SetShowStatusBar(false) + + m := model{list: l} + + if _, er := tea.NewProgram(&m, tea.WithAltScreen()).Run(); er != nil { + return nil, er + } + + return m.choice, nil +} From 94c51dfb6c896494b8cb6fc6540c212f2f347ebd Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Tue, 17 Dec 2024 16:03:08 +0530 Subject: [PATCH 18/26] change list styles to match zopdev theme --- environment/service/env.go | 4 +++- utils/list.go | 16 +++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/environment/service/env.go b/environment/service/env.go index 93527e4..bb0517f 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -12,6 +12,8 @@ import ( "zop.dev/cli/zop/utils" ) +const listTitle = "Select the application where you want to add the environment!" + var ( // ErrUnableToRenderApps is returned when the application list cannot be rendered. ErrUnableToRenderApps = errors.New("unable to render the list of applications") @@ -122,7 +124,7 @@ func (s *Service) getSelectedApplication(ctx *gofr.Context) (*utils.Item, error) items = append(items, &utils.Item{ID: app.ID, Name: app.Name}) } - choice, err := utils.RenderList(items) + choice, err := utils.RenderList(listTitle, items) if err != nil { ctx.Logger.Errorf("unable to render the list of applications! %v", err) diff --git a/utils/list.go b/utils/list.go index a94e98a..fa58938 100644 --- a/utils/list.go +++ b/utils/list.go @@ -21,15 +21,17 @@ const ( var ( // itemStyle defines the default style for list items. itemStyle = lipgloss.NewStyle().PaddingLeft(listPaddingLeft) + // selectedItemStyle defines the style for the selected list item. - selectedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("170")) + selectedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#06b6d4")) + // paginationStyle defines the style for pagination controls. paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(paginationPadding) - // helpStyle defines the style for the help text. - helpStyle = list.DefaultStyles().HelpStyle + + titleStyle = lipgloss.NewStyle().Background(lipgloss.Color("#0891b2")).Foreground(lipgloss.Color("#ffffff")) ) -// item represents a single item in the list. +// Item represents a single item in the list. type Item struct { ID int // ID is the unique identifier for the item. Name string // Name is the display name of the item. @@ -120,7 +122,7 @@ func (m *model) View() string { return "\n" + m.list.View() } -func RenderList(items []*Item) (*Item, error) { +func RenderList(title string, items []*Item) (*Item, error) { listItems := make([]list.Item, 0) for i := range items { @@ -128,11 +130,11 @@ func RenderList(items []*Item) (*Item, error) { } l := list.New(listItems, itemDelegate{}, listWidth, listHeight) - l.Title = "Select the application where you want to add the environment!" + l.Title = title + l.Styles.Title = titleStyle l.SetShowStatusBar(false) l.SetFilteringEnabled(true) l.Styles.PaginationStyle = paginationStyle - l.Styles.HelpStyle = helpStyle l.SetShowStatusBar(false) m := model{list: l} From 2d4803c5f451e8d14a491a217a3b85c54c3b0764 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Tue, 17 Dec 2024 17:07:24 +0530 Subject: [PATCH 19/26] add selection of cloud acc, application and deploy options --- application/service/models.go | 2 +- deploymentspace/service/deployment.go | 151 ++++++++++++++++++++++---- deploymentspace/service/errors.go | 11 ++ deploymentspace/service/interface.go | 7 +- deploymentspace/service/models.go | 7 ++ environment/service/env.go | 2 +- utils/list.go | 2 +- utils/response.go | 21 ++++ 8 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 deploymentspace/service/errors.go create mode 100644 deploymentspace/service/models.go create mode 100644 utils/response.go diff --git a/application/service/models.go b/application/service/models.go index afbde97..efdc1e3 100644 --- a/application/service/models.go +++ b/application/service/models.go @@ -67,7 +67,7 @@ type Environment struct { // Application represents an application with its associated environments. type Application struct { - ID int `json:"id"` // Unique identifier of the application + ID int64 `json:"id"` // Unique identifier of the application Name string `json:"name"` // Name of the application Envs []Environment `json:"environments,omitempty"` // List of associated environments } diff --git a/deploymentspace/service/deployment.go b/deploymentspace/service/deployment.go index 0f9d6ff..d9bb080 100644 --- a/deploymentspace/service/deployment.go +++ b/deploymentspace/service/deployment.go @@ -1,53 +1,160 @@ package service -import "gofr.dev/pkg/gofr" +import ( + "errors" + "fmt" + + "gofr.dev/pkg/gofr" + + cloudSvc "zop.dev/cli/zop/cloud/service/list" + "zop.dev/cli/zop/utils" +) + +const ( + accListTitle = "Select the cloud account where you want to add the deployment!" + appListTitle = "Select the application where you want to add the deployment!" + deploymentSpaceTitle = "Select the deployment space where you want to add the deployment!" +) + +var ( + // ErrUnableToRenderList is returned when the application list cannot be rendered. + ErrUnableToRenderList = errors.New("unable to render the list") + + // ErrConnectingZopAPI is returned when there is an error connecting to the Zop API. + ErrConnectingZopAPI = errors.New("unable to connect to Zop API") + + // ErrGettingDeploymentOptions is returned when there is an error adding an environment. + ErrGettingDeploymentOptions = errors.New("unable to get deployment options") + + // ErrorFetchingEnvironments is returned when there is an error fetching environments for a given application. + ErrorFetchingEnvironments = errors.New("unable to fetch environments") +) type Service struct { cloudGet CloudAccountService appGet ApplicationService + envGet EnvironmentService } -func New() *Service { - return &Service{} +func New(cloudGet CloudAccountService, appGet ApplicationService, envGet EnvironmentService) *Service { + return &Service{ + cloudGet: cloudGet, + appGet: appGet, + envGet: envGet, + } } -func (*Service) Add(ctx *gofr.Context) error { - apiSvc := ctx.GetHTTPService("api-service") +func (s *Service) Add(ctx *gofr.Context) error { + //var request = make(map[string]any) + + // apiSvc := ctx.GetHTTPService("api-service") + cloudAcc, err := s.getSelectedCloudAccount(ctx) + if err != nil { + return err + } + + //request["cloud"] + + ctx.Out.Println("Selected cloud account: ", cloudAcc.Name) + + app, err := s.getSelectedApplication(ctx) + if err != nil { + return err + } + + ctx.Out.Println("Selected application: ", app.Name) return nil } -func (s *Service) getSelectedApplication(ctx *gofr.Context) (*item, error) { +func (s *Service) getSelectedCloudAccount(ctx *gofr.Context) (*cloudSvc.CloudAccountResponse, error) { + accounts, err := s.cloudGet.GetAccounts(ctx) + if err != nil { + ctx.Logger.Errorf("unable to fetch cloud accounts! %v", err) + } + + items := make([]*utils.Item, 0) + for _, acc := range accounts { + items = append(items, &utils.Item{ID: acc.ID, Name: acc.Name, Data: acc}) + } + + choice, err := utils.RenderList(accListTitle, items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of cloud accounts! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{"cloud account"} + } + + return choice.Data.(*cloudSvc.CloudAccountResponse), nil +} + +func (s *Service) getSelectedApplication(ctx *gofr.Context) (*utils.Item, error) { apps, err := s.appGet.List(ctx) if err != nil { return nil, err } - items := make([]list.Item, 0) + items := make([]*utils.Item, 0) for _, app := range apps { - items = append(items, &item{app.ID, app.Name}) + items = append(items, &utils.Item{ID: app.ID, Name: app.Name}) } - l := list.New(items, itemDelegate{}, listWidth, listHeight) - l.Title = "Select the application where you want to add the environment!" - l.SetShowStatusBar(false) - l.SetFilteringEnabled(true) - l.Styles.PaginationStyle = paginationStyle - l.Styles.HelpStyle = helpStyle - l.SetShowStatusBar(false) + choice, err := utils.RenderList(appListTitle, items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of applications! %v", err) + + return nil, ErrUnableToRenderList + } - m := model{list: l} + if choice == nil { + return nil, &ErrNoItemSelected{"application"} + } + + return choice, nil +} + +func (s *Service) getSelectedOptions(ctx *gofr.Context, id int64) (*DeploymentSpaceOptions, error) { + resp, err := ctx.GetHTTPService("api-service"). + Get(ctx, fmt.Sprintf("cloud-accounts/%d/deployment-space/options", id), nil) + + if err != nil { + ctx.Logger.Errorf("error connecting to zop api! %v", err) - if _, er := tea.NewProgram(&m).Run(); er != nil { - ctx.Logger.Errorf("unable to render the list of applications! %v", er) + return nil, ErrConnectingZopAPI + } + + var opts struct { + Options []*DeploymentSpaceOptions `json:"data"` + } + + err = utils.GetResponse(resp, &opts) + if err != nil { + ctx.Logger.Errorf("error fetching deployment space options! %v", err) + + return nil, ErrGettingDeploymentOptions + } + + items := make([]*utils.Item, 0) + + for _, opt := range opts.Options { + items = append(items, &utils.Item{Name: opt.Name, Data: opt}) + } + + choice, err := utils.RenderList(deploymentSpaceTitle, items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of deployment spaces! %v", err) - return nil, ErrUnableToRenderApps + return nil, ErrUnableToRenderList } - if m.choice == nil { - return nil, ErrNoApplicationSelected + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{"deployment space"} } - return m.choice, nil + return choice.Data.(*DeploymentSpaceOptions), nil } diff --git a/deploymentspace/service/errors.go b/deploymentspace/service/errors.go new file mode 100644 index 0000000..efb2b00 --- /dev/null +++ b/deploymentspace/service/errors.go @@ -0,0 +1,11 @@ +package service + +import "fmt" + +type ErrNoItemSelected struct { + Type string +} + +func (e *ErrNoItemSelected) Error() string { + return fmt.Sprintf("no %s selected", e.Type) +} diff --git a/deploymentspace/service/interface.go b/deploymentspace/service/interface.go index d5b8901..b2189b1 100644 --- a/deploymentspace/service/interface.go +++ b/deploymentspace/service/interface.go @@ -5,6 +5,7 @@ import ( appSvc "zop.dev/cli/zop/application/service" cloudSvc "zop.dev/cli/zop/cloud/service/list" + envSvc "zop.dev/cli/zop/environment/service" ) type CloudAccountService interface { @@ -15,6 +16,6 @@ type ApplicationService interface { List(ctx *gofr.Context) ([]appSvc.Application, error) } -//type EnvironmentService interface { -// List(ctx *gofr.Context) ([]Environment, error) -//} +type EnvironmentService interface { + List(ctx *gofr.Context) ([]envSvc.Environment, error) +} diff --git a/deploymentspace/service/models.go b/deploymentspace/service/models.go new file mode 100644 index 0000000..5b2d5e5 --- /dev/null +++ b/deploymentspace/service/models.go @@ -0,0 +1,7 @@ +package service + +type DeploymentSpaceOptions struct { + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` +} diff --git a/environment/service/env.go b/environment/service/env.go index bb0517f..d262416 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -63,7 +63,7 @@ func (s *Service) Add(ctx *gofr.Context) (int, error) { _, _ = fmt.Scanf("%s", &input) - err = postEnvironment(ctx, &Environment{Name: input, Level: level, ApplicationID: int64(app.ID)}) + err = postEnvironment(ctx, &Environment{Name: input, Level: level, ApplicationID: app.ID}) if err != nil { return level, err } diff --git a/utils/list.go b/utils/list.go index fa58938..4318e08 100644 --- a/utils/list.go +++ b/utils/list.go @@ -33,7 +33,7 @@ var ( // Item represents a single item in the list. type Item struct { - ID int // ID is the unique identifier for the item. + ID int64 // ID is the unique identifier for the item. Name string // Name is the display name of the item. Data any } diff --git a/utils/response.go b/utils/response.go new file mode 100644 index 0000000..6af80e1 --- /dev/null +++ b/utils/response.go @@ -0,0 +1,21 @@ +package utils + +import ( + "encoding/json" + "io" + "net/http" +) + +// GetResponse reads the HTTP response body and unmarshals it into the provided interface. +func GetResponse(resp *http.Response, i any) error { + defer resp.Body.Close() + + b, _ := io.ReadAll(resp.Body) + + err := json.Unmarshal(b, i) + if err != nil { + return err + } + + return nil +} From bed8edef376babe3a233901c2ebd68da26845a3e Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 18 Dec 2024 04:48:49 +0530 Subject: [PATCH 20/26] Display selected application --- environment/service/env.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/environment/service/env.go b/environment/service/env.go index d262416..6a59d05 100644 --- a/environment/service/env.go +++ b/environment/service/env.go @@ -12,7 +12,7 @@ import ( "zop.dev/cli/zop/utils" ) -const listTitle = "Select the application where you want to add the environment!" +const listTitle = "Select the application!" var ( // ErrUnableToRenderApps is returned when the application list cannot be rendered. @@ -88,6 +88,8 @@ func (s *Service) List(ctx *gofr.Context) ([]Environment, error) { return nil, err } + ctx.Out.Println("Selected application: ", app.Name) + resp, err := ctx.GetHTTPService("api-service"). Get(ctx, fmt.Sprintf("applications/%d/environments", app.ID), nil) if err != nil { From d0c99053d177585c493752d5b50151767910a314 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 18 Dec 2024 04:49:46 +0530 Subject: [PATCH 21/26] Add logic to display different option list for deployment space --- deploymentspace/handler/deployment.go | 17 ++- deploymentspace/service/deployment.go | 202 +++++++++++++++++++++----- deploymentspace/service/interface.go | 5 - deploymentspace/service/models.go | 15 ++ main.go | 8 +- 5 files changed, 203 insertions(+), 44 deletions(-) diff --git a/deploymentspace/handler/deployment.go b/deploymentspace/handler/deployment.go index a960ab8..f70dd0a 100644 --- a/deploymentspace/handler/deployment.go +++ b/deploymentspace/handler/deployment.go @@ -2,13 +2,22 @@ package handler import "gofr.dev/pkg/gofr" -type Handler struct{} +type Handler struct { + deployService DeploymentService +} -func New() *Handler { - return &Handler{} +func New(depSvc DeploymentService) *Handler { + return &Handler{ + deployService: depSvc, + } } func (h *Handler) Add(ctx *gofr.Context) (any, error) { // Add your code here - return nil, nil + err := h.deployService.Add(ctx) + if err != nil { + return nil, err + } + + return "Deployment Created", nil } diff --git a/deploymentspace/service/deployment.go b/deploymentspace/service/deployment.go index d9bb080..c6ab094 100644 --- a/deploymentspace/service/deployment.go +++ b/deploymentspace/service/deployment.go @@ -1,8 +1,12 @@ package service import ( + "encoding/json" "errors" "fmt" + "net/http" + "strings" + envSvc "zop.dev/cli/zop/environment/service" "gofr.dev/pkg/gofr" @@ -12,7 +16,6 @@ import ( const ( accListTitle = "Select the cloud account where you want to add the deployment!" - appListTitle = "Select the application where you want to add the deployment!" deploymentSpaceTitle = "Select the deployment space where you want to add the deployment!" ) @@ -28,97 +31,165 @@ var ( // ErrorFetchingEnvironments is returned when there is an error fetching environments for a given application. ErrorFetchingEnvironments = errors.New("unable to fetch environments") + + // ErrNoOptionsFound is returned when there are no options available for selection. + ErrNoOptionsFound = errors.New("no options available for selection") ) type Service struct { cloudGet CloudAccountService - appGet ApplicationService envGet EnvironmentService } -func New(cloudGet CloudAccountService, appGet ApplicationService, envGet EnvironmentService) *Service { +func New(cloudGet CloudAccountService, envGet EnvironmentService) *Service { return &Service{ cloudGet: cloudGet, - appGet: appGet, envGet: envGet, } } func (s *Service) Add(ctx *gofr.Context) error { - //var request = make(map[string]any) + var request = make(map[string]any) - // apiSvc := ctx.GetHTTPService("api-service") cloudAcc, err := s.getSelectedCloudAccount(ctx) if err != nil { return err } - //request["cloud"] + request["cloudAccount"] = cloudAcc ctx.Out.Println("Selected cloud account: ", cloudAcc.Name) - app, err := s.getSelectedApplication(ctx) + env, err := s.getSelectedEnvironment(ctx) if err != nil { return err } - ctx.Out.Println("Selected application: ", app.Name) + ctx.Out.Println("Selected environment"+ + ":", env.Name) - return nil + options, err := s.getDeploymentSpaceOptions(ctx, cloudAcc.ID) + if err != nil { + return err + } + + request[options.Type] = options + + if er := s.processOptions(ctx, request, options.Path); er != nil { + return er + } + + return s.submitDeployment(ctx, env.ID, request) } -func (s *Service) getSelectedCloudAccount(ctx *gofr.Context) (*cloudSvc.CloudAccountResponse, error) { - accounts, err := s.cloudGet.GetAccounts(ctx) +func (s *Service) processOptions(ctx *gofr.Context, request map[string]any, path string) error { + api := ctx.GetHTTPService("api-service") + + resp, err := api.Get(ctx, path[1:], nil) if err != nil { - ctx.Logger.Errorf("unable to fetch cloud accounts! %v", err) + ctx.Logger.Errorf("error connecting to zop api! %v", err) + return ErrConnectingZopAPI } - items := make([]*utils.Item, 0) - for _, acc := range accounts { - items = append(items, &utils.Item{ID: acc.ID, Name: acc.Name, Data: acc}) + var option apiResponse + if er := utils.GetResponse(resp, &option); er != nil { + ctx.Logger.Errorf("error fetching deployment options! %v", er) + + return ErrGettingDeploymentOptions } - choice, err := utils.RenderList(accListTitle, items) + for { + opt, er := s.getSelectedOption(ctx, option.Data.Option) + if er != nil { + return er + } + + s.updateRequestWithOption(request, opt) + + if option.Data.Next == nil { + break + } + + params := getParameters(opt, &option) + resp, er = api.Get(ctx, option.Data.Next.Path[1:]+params, nil) + if er != nil { + ctx.Logger.Errorf("error connecting to zop api! %v", er) + return ErrConnectingZopAPI + } + + er = utils.GetResponse(resp, &option) + if er != nil { + ctx.Logger.Errorf("error fetching deployment options! %v", er) + return ErrGettingDeploymentOptions + } + } + + return nil +} + +func (s *Service) updateRequestWithOption(request map[string]any, opt map[string]any) { + keys := strings.Split(opt["type"].(string), ".") + current := request + + for i, key := range keys { + if i == len(keys)-1 { + current[key] = opt + break + } + + if _, exists := current[key]; !exists { + current[key] = make(map[string]any) + } + current = current[key].(map[string]any) + } +} + +func (s *Service) submitDeployment(ctx *gofr.Context, envID int64, request map[string]any) error { + b, err := json.Marshal(request) if err != nil { - ctx.Logger.Errorf("unable to render the list of cloud accounts! %v", err) + return err + } - return nil, ErrUnableToRenderList + resp, err := ctx.GetHTTPService("api-service").PostWithHeaders(ctx, fmt.Sprintf("environments/%d/deploymentspace", envID), nil, b, map[string]string{ + "Content-Type": "application/json", + }) + if err != nil { + return ErrConnectingZopAPI } - if choice == nil || choice.Data == nil { - return nil, &ErrNoItemSelected{"cloud account"} + if resp.StatusCode != http.StatusCreated { + return errors.New("error adding deployment") } - return choice.Data.(*cloudSvc.CloudAccountResponse), nil + return nil } -func (s *Service) getSelectedApplication(ctx *gofr.Context) (*utils.Item, error) { - apps, err := s.appGet.List(ctx) +func (s *Service) getSelectedCloudAccount(ctx *gofr.Context) (*cloudSvc.CloudAccountResponse, error) { + accounts, err := s.cloudGet.GetAccounts(ctx) if err != nil { - return nil, err + ctx.Logger.Errorf("unable to fetch cloud accounts! %v", err) } items := make([]*utils.Item, 0) - - for _, app := range apps { - items = append(items, &utils.Item{ID: app.ID, Name: app.Name}) + for _, acc := range accounts { + items = append(items, &utils.Item{ID: acc.ID, Name: acc.Name, Data: acc}) } - choice, err := utils.RenderList(appListTitle, items) + choice, err := utils.RenderList(accListTitle, items) if err != nil { - ctx.Logger.Errorf("unable to render the list of applications! %v", err) + ctx.Logger.Errorf("unable to render the list of cloud accounts! %v", err) return nil, ErrUnableToRenderList } - if choice == nil { - return nil, &ErrNoItemSelected{"application"} + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{"cloud account"} } - return choice, nil + return choice.Data.(*cloudSvc.CloudAccountResponse), nil } -func (s *Service) getSelectedOptions(ctx *gofr.Context, id int64) (*DeploymentSpaceOptions, error) { +func (s *Service) getDeploymentSpaceOptions(ctx *gofr.Context, id int64) (*DeploymentSpaceOptions, error) { resp, err := ctx.GetHTTPService("api-service"). Get(ctx, fmt.Sprintf("cloud-accounts/%d/deployment-space/options", id), nil) @@ -158,3 +229,66 @@ func (s *Service) getSelectedOptions(ctx *gofr.Context, id int64) (*DeploymentSp return choice.Data.(*DeploymentSpaceOptions), nil } + +func (s *Service) getSelectedEnvironment(ctx *gofr.Context) (*envSvc.Environment, error) { + envs, err := s.envGet.List(ctx) + if err != nil { + ctx.Logger.Errorf("unable to fetch environments! %v", err) + + return nil, ErrorFetchingEnvironments + } + + items := make([]*utils.Item, 0) + + for _, env := range envs { + items = append(items, &utils.Item{ID: env.ID, Name: env.Name, Data: &env}) + } + + choice, err := utils.RenderList("Select the environment where you want to add the deployment!", items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of environments! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil { + return nil, &ErrNoItemSelected{"environment"} + } + + return choice.Data.(*envSvc.Environment), nil +} + +func (s *Service) getSelectedOption(ctx *gofr.Context, items []map[string]any) (map[string]any, error) { + listI := make([]*utils.Item, 0) + + if len(items) == 0 { + return nil, ErrNoOptionsFound + } + + for _, item := range items { + listI = append(listI, &utils.Item{Name: item["name"].(string), Data: item}) + } + + choice, err := utils.RenderList("Select the option", listI) + if err != nil { + ctx.Logger.Errorf("unable to render the list of environments! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{items[0]["type"].(string)} + } + + return choice.Data.(map[string]any), nil +} + +func getParameters(opt map[string]any, options *apiResponse) string { + params := "?" + + for _, v := range options.Data.Next.Params { + params += fmt.Sprintf("&%s=%s", v, opt[v]) + } + + return params +} diff --git a/deploymentspace/service/interface.go b/deploymentspace/service/interface.go index b2189b1..ba3666a 100644 --- a/deploymentspace/service/interface.go +++ b/deploymentspace/service/interface.go @@ -3,7 +3,6 @@ package service import ( "gofr.dev/pkg/gofr" - appSvc "zop.dev/cli/zop/application/service" cloudSvc "zop.dev/cli/zop/cloud/service/list" envSvc "zop.dev/cli/zop/environment/service" ) @@ -12,10 +11,6 @@ type CloudAccountService interface { GetAccounts(ctx *gofr.Context) ([]*cloudSvc.CloudAccountResponse, error) } -type ApplicationService interface { - List(ctx *gofr.Context) ([]appSvc.Application, error) -} - type EnvironmentService interface { List(ctx *gofr.Context) ([]envSvc.Environment, error) } diff --git a/deploymentspace/service/models.go b/deploymentspace/service/models.go index 5b2d5e5..9f33050 100644 --- a/deploymentspace/service/models.go +++ b/deploymentspace/service/models.go @@ -5,3 +5,18 @@ type DeploymentSpaceOptions struct { Path string `json:"path"` Type string `json:"type"` } + +type DeploymentOption struct { + Option []map[string]any `json:"options"` + Next *NextPage `json:"nextPage"` +} + +type NextPage struct { + Name string `json:"name"` + Path string `json:"path"` + Params map[string]string `json:"params"` +} + +type apiResponse struct { + Data *DeploymentOption `json:"data"` +} diff --git a/main.go b/main.go index b864955..df94dac 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( _ "github.com/mattn/go-sqlite3" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/service" - _ "modernc.org/sqlite" applicationHandler "zop.dev/cli/zop/application/handler" applicationSvc "zop.dev/cli/zop/application/service" @@ -17,6 +16,8 @@ import ( impService "zop.dev/cli/zop/cloud/service/gcp" listSvc "zop.dev/cli/zop/cloud/service/list" impStore "zop.dev/cli/zop/cloud/store/gcp" + depHandler "zop.dev/cli/zop/deploymentspace/handler" + depSvc "zop.dev/cli/zop/deploymentspace/service" envHandler "zop.dev/cli/zop/environment/handler" envService "zop.dev/cli/zop/environment/service" ) @@ -67,5 +68,10 @@ func main() { app.SubCommand("environment add", envH.Add) app.SubCommand("environment list", envH.List) + dSvc := depSvc.New(lSvc, envSvc) + dH := depHandler.New(dSvc) + + app.SubCommand("deployment add", dH.Add) + app.Run() } From 6441061c98f91fd619cfa459ff6cdafe23cba595 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 18 Dec 2024 13:04:48 +0530 Subject: [PATCH 22/26] refactor service and add godoc --- deploymentspace/handler/deployment.go | 19 ++++ deploymentspace/handler/interface.go | 10 ++ deploymentspace/service/deployment.go | 152 ++++---------------------- deploymentspace/service/errors.go | 7 ++ deploymentspace/service/interface.go | 20 ++++ deploymentspace/service/listings.go | 145 ++++++++++++++++++++++++ deploymentspace/service/models.go | 28 +++-- 7 files changed, 240 insertions(+), 141 deletions(-) create mode 100644 deploymentspace/service/listings.go diff --git a/deploymentspace/handler/deployment.go b/deploymentspace/handler/deployment.go index 45e61f4..867a82d 100644 --- a/deploymentspace/handler/deployment.go +++ b/deploymentspace/handler/deployment.go @@ -1,17 +1,36 @@ +// Package handler is used to import data from external sources +// this package has an Add(ctx *gofr.Context) method that is used to configure deployment space +// for the environments of applications. package handler import "gofr.dev/pkg/gofr" +// Handler is responsible for handling requests related to deployment operations. type Handler struct { deployService DeploymentService } +// New initializes a new Handler instance. +// +// Parameters: +// - depSvc: An instance of DeploymentService used to manage deployment-related operations. +// +// Returns: +// - A pointer to the Handler instance. func New(depSvc DeploymentService) *Handler { return &Handler{ deployService: depSvc, } } +// Add processes a deployment creation request. +// +// Parameters: +// - ctx: The context object containing request and session details. +// +// Returns: +// - A success message if the deployment is created successfully. +// - An error if the deployment creation fails. func (h *Handler) Add(ctx *gofr.Context) (any, error) { err := h.deployService.Add(ctx) if err != nil { diff --git a/deploymentspace/handler/interface.go b/deploymentspace/handler/interface.go index 2eb91d6..ffc4f4e 100644 --- a/deploymentspace/handler/interface.go +++ b/deploymentspace/handler/interface.go @@ -2,6 +2,16 @@ package handler import "gofr.dev/pkg/gofr" +// DeploymentService defines the interface for deployment-related operations. +// +// It contains methods that allow adding and managing deployments. type DeploymentService interface { + // Add creates a new deployment. + // + // Parameters: + // - ctx: The context object containing request and session details. + // + // Returns: + // - An error if the deployment creation fails. Add(ctx *gofr.Context) error } diff --git a/deploymentspace/service/deployment.go b/deploymentspace/service/deployment.go index 0270207..66750ad 100644 --- a/deploymentspace/service/deployment.go +++ b/deploymentspace/service/deployment.go @@ -1,3 +1,4 @@ +// Package service provides structures and interfaces for managing deployment options and related operations. package service import ( @@ -9,19 +10,10 @@ import ( "gofr.dev/pkg/gofr" - cloudSvc "zop.dev/cli/zop/cloud/service/list" - envSvc "zop.dev/cli/zop/environment/service" "zop.dev/cli/zop/utils" ) -const ( - accListTitle = "Select the cloud account where you want to add the deployment!" - deploymentSpaceTitle = "Select the deployment space where you want to add the deployment!" -) - var ( - // ErrUnableToRenderList is returned when the application list cannot be rendered. - ErrUnableToRenderList = errors.New("unable to render the list") // ErrConnectingZopAPI is returned when there is an error connecting to the Zop API. ErrConnectingZopAPI = errors.New("unable to connect to Zop API") @@ -31,16 +23,22 @@ var ( // ErrorFetchingEnvironments is returned when there is an error fetching environments for a given application. ErrorFetchingEnvironments = errors.New("unable to fetch environments") - - // ErrNoOptionsFound is returned when there are no options available for selection. - ErrNoOptionsFound = errors.New("no options available for selection") ) +// Service represents the core service that handles cloud account and environment-related operations. type Service struct { cloudGet CloudAccountService envGet EnvironmentService } +// New initializes a new Service instance. +// +// Parameters: +// - cloudGet: A CloudAccountService instance for retrieving cloud accounts. +// - envGet: An EnvironmentService instance for retrieving environments. +// +// Returns: +// - A pointer to the Service instance. func New(cloudGet CloudAccountService, envGet EnvironmentService) *Service { return &Service{ cloudGet: cloudGet, @@ -48,6 +46,16 @@ func New(cloudGet CloudAccountService, envGet EnvironmentService) *Service { } } +// Add handles the addition of a deployment configuration. +// +// This function selects a cloud account and environment, retrieves deployment options, +// processes the options, and submits the deployment request. +// +// Parameters: +// - ctx: The context object containing request and session details. +// +// Returns: +// - An error if any step in the process fails. func (s *Service) Add(ctx *gofr.Context) error { var request = make(map[string]any) @@ -171,126 +179,6 @@ func submitDeployment(ctx *gofr.Context, envID int64, request map[string]any) er return resp.Body.Close() } -func (s *Service) getSelectedCloudAccount(ctx *gofr.Context) (*cloudSvc.CloudAccountResponse, error) { - accounts, err := s.cloudGet.GetAccounts(ctx) - if err != nil { - ctx.Logger.Errorf("unable to fetch cloud accounts! %v", err) - } - - items := make([]*utils.Item, 0) - for _, acc := range accounts { - items = append(items, &utils.Item{ID: acc.ID, Name: acc.Name, Data: acc}) - } - - choice, err := utils.RenderList(accListTitle, items) - if err != nil { - ctx.Logger.Errorf("unable to render the list of cloud accounts! %v", err) - - return nil, ErrUnableToRenderList - } - - if choice == nil || choice.Data == nil { - return nil, &ErrNoItemSelected{"cloud account"} - } - - return choice.Data.(*cloudSvc.CloudAccountResponse), nil -} - -func getDeploymentSpaceOptions(ctx *gofr.Context, id int64) (*DeploymentSpaceOptions, error) { - resp, err := ctx.GetHTTPService("api-service"). - Get(ctx, fmt.Sprintf("cloud-accounts/%d/deployment-space/options", id), nil) - if err != nil { - ctx.Logger.Errorf("error connecting to zop api! %v", err) - - return nil, ErrConnectingZopAPI - } - - defer resp.Body.Close() - - var opts struct { - Options []*DeploymentSpaceOptions `json:"data"` - } - - err = utils.GetResponse(resp, &opts) - if err != nil { - ctx.Logger.Errorf("error fetching deployment space options! %v", err) - - return nil, ErrGettingDeploymentOptions - } - - items := make([]*utils.Item, 0) - - for _, opt := range opts.Options { - items = append(items, &utils.Item{Name: opt.Name, Data: opt}) - } - - choice, err := utils.RenderList(deploymentSpaceTitle, items) - if err != nil { - ctx.Logger.Errorf("unable to render the list of deployment spaces! %v", err) - - return nil, ErrUnableToRenderList - } - - if choice == nil || choice.Data == nil { - return nil, &ErrNoItemSelected{"deployment space"} - } - - return choice.Data.(*DeploymentSpaceOptions), nil -} - -func (s *Service) getSelectedEnvironment(ctx *gofr.Context) (*envSvc.Environment, error) { - envs, err := s.envGet.List(ctx) - if err != nil { - ctx.Logger.Errorf("unable to fetch environments! %v", err) - - return nil, ErrorFetchingEnvironments - } - - items := make([]*utils.Item, 0) - - for _, env := range envs { - items = append(items, &utils.Item{ID: env.ID, Name: env.Name, Data: &env}) - } - - choice, err := utils.RenderList("Select the environment where you want to add the deployment!", items) - if err != nil { - ctx.Logger.Errorf("unable to render the list of environments! %v", err) - - return nil, ErrUnableToRenderList - } - - if choice == nil { - return nil, &ErrNoItemSelected{"environment"} - } - - return choice.Data.(*envSvc.Environment), nil -} - -func getSelectedOption(ctx *gofr.Context, items []map[string]any) (map[string]any, error) { - listI := make([]*utils.Item, 0) - - if len(items) == 0 { - return nil, ErrNoOptionsFound - } - - for _, item := range items { - listI = append(listI, &utils.Item{Name: item["name"].(string), Data: item}) - } - - choice, err := utils.RenderList("Select the option", listI) - if err != nil { - ctx.Logger.Errorf("unable to render the list of environments! %v", err) - - return nil, ErrUnableToRenderList - } - - if choice == nil || choice.Data == nil { - return nil, &ErrNoItemSelected{items[0]["type"].(string)} - } - - return choice.Data.(map[string]any), nil -} - func getParameters(opt map[string]any, options *apiResponse) string { params := "?" diff --git a/deploymentspace/service/errors.go b/deploymentspace/service/errors.go index efb2b00..9e2c6ce 100644 --- a/deploymentspace/service/errors.go +++ b/deploymentspace/service/errors.go @@ -2,10 +2,17 @@ package service import "fmt" +// ErrNoItemSelected represents an error that occurs when no item of a specific type is selected. type ErrNoItemSelected struct { Type string } +// Error returns the error message for ErrNoItemSelected. +// +// This method satisfies the error interface. +// +// Returns: +// - A formatted error message indicating the type of the unselected item. func (e *ErrNoItemSelected) Error() string { return fmt.Sprintf("no %s selected", e.Type) } diff --git a/deploymentspace/service/interface.go b/deploymentspace/service/interface.go index ba3666a..c763c3d 100644 --- a/deploymentspace/service/interface.go +++ b/deploymentspace/service/interface.go @@ -7,10 +7,30 @@ import ( envSvc "zop.dev/cli/zop/environment/service" ) +// CloudAccountService defines the interface for managing cloud accounts. +// It provides methods to retrieve cloud accounts available to the user. type CloudAccountService interface { + // GetAccounts retrieves a list of cloud accounts. + // + // Parameters: + // - ctx: The context object containing request and session details. + // + // Returns: + // - A slice of pointers to CloudAccountResponse objects. + // - An error if the retrieval fails. GetAccounts(ctx *gofr.Context) ([]*cloudSvc.CloudAccountResponse, error) } +// EnvironmentService defines the interface for managing environments. +// It provides methods to list the environments associated with a user. type EnvironmentService interface { + // List retrieves a list of environments. + // + // Parameters: + // - ctx: The context object containing request and session details. + // + // Returns: + // - A slice of Environment objects. + // - An error if the retrieval fails. List(ctx *gofr.Context) ([]envSvc.Environment, error) } diff --git a/deploymentspace/service/listings.go b/deploymentspace/service/listings.go new file mode 100644 index 0000000..de5b2e5 --- /dev/null +++ b/deploymentspace/service/listings.go @@ -0,0 +1,145 @@ +package service + +import ( + "errors" + "fmt" + + "gofr.dev/pkg/gofr" + + cloudSvc "zop.dev/cli/zop/cloud/service/list" + envSvc "zop.dev/cli/zop/environment/service" + "zop.dev/cli/zop/utils" +) + +const ( + accListTitle = "Select the cloud account where you want to add the deployment!" + deploymentSpaceTitle = "Select the deployment space where you want to add the deployment!" +) + +var ( + // ErrUnableToRenderList is returned when the application list cannot be rendered. + ErrUnableToRenderList = errors.New("unable to render the list") + + // ErrNoOptionsFound is returned when there are no options available for selection. + ErrNoOptionsFound = errors.New("no options available for selection") +) + +func (s *Service) getSelectedCloudAccount(ctx *gofr.Context) (*cloudSvc.CloudAccountResponse, error) { + accounts, err := s.cloudGet.GetAccounts(ctx) + if err != nil { + ctx.Logger.Errorf("unable to fetch cloud accounts! %v", err) + } + + items := make([]*utils.Item, 0) + for _, acc := range accounts { + items = append(items, &utils.Item{ID: acc.ID, Name: acc.Name, Data: acc}) + } + + choice, err := utils.RenderList(accListTitle, items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of cloud accounts! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{"cloud account"} + } + + return choice.Data.(*cloudSvc.CloudAccountResponse), nil +} + +func (s *Service) getSelectedEnvironment(ctx *gofr.Context) (*envSvc.Environment, error) { + envs, err := s.envGet.List(ctx) + if err != nil { + ctx.Logger.Errorf("unable to fetch environments! %v", err) + + return nil, ErrorFetchingEnvironments + } + + items := make([]*utils.Item, 0) + + for _, env := range envs { + items = append(items, &utils.Item{ID: env.ID, Name: env.Name, Data: &env}) + } + + choice, err := utils.RenderList("Select the environment where you want to add the deployment!", items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of environments! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil { + return nil, &ErrNoItemSelected{"environment"} + } + + return choice.Data.(*envSvc.Environment), nil +} + +func getDeploymentSpaceOptions(ctx *gofr.Context, id int64) (*DeploymentSpaceOptions, error) { + resp, err := ctx.GetHTTPService("api-service"). + Get(ctx, fmt.Sprintf("cloud-accounts/%d/deployment-space/options", id), nil) + if err != nil { + ctx.Logger.Errorf("error connecting to zop api! %v", err) + + return nil, ErrConnectingZopAPI + } + + defer resp.Body.Close() + + var opts struct { + Options []*DeploymentSpaceOptions `json:"data"` + } + + err = utils.GetResponse(resp, &opts) + if err != nil { + ctx.Logger.Errorf("error fetching deployment space options! %v", err) + + return nil, ErrGettingDeploymentOptions + } + + items := make([]*utils.Item, 0) + + for _, opt := range opts.Options { + items = append(items, &utils.Item{Name: opt.Name, Data: opt}) + } + + choice, err := utils.RenderList(deploymentSpaceTitle, items) + if err != nil { + ctx.Logger.Errorf("unable to render the list of deployment spaces! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{"deployment space"} + } + + return choice.Data.(*DeploymentSpaceOptions), nil +} + +func getSelectedOption(ctx *gofr.Context, items []map[string]any) (map[string]any, error) { + listI := make([]*utils.Item, 0) + + if len(items) == 0 { + return nil, ErrNoOptionsFound + } + + for _, item := range items { + listI = append(listI, &utils.Item{Name: item["name"].(string), Data: item}) + } + + choice, err := utils.RenderList("Select the option", listI) + if err != nil { + ctx.Logger.Errorf("unable to render the list of environments! %v", err) + + return nil, ErrUnableToRenderList + } + + if choice == nil || choice.Data == nil { + return nil, &ErrNoItemSelected{items[0]["type"].(string)} + } + + return choice.Data.(map[string]any), nil +} diff --git a/deploymentspace/service/models.go b/deploymentspace/service/models.go index 9f33050..8df3d5d 100644 --- a/deploymentspace/service/models.go +++ b/deploymentspace/service/models.go @@ -1,22 +1,32 @@ package service +// DeploymentSpaceOptions represents the deployment space options in the system. +// +// It includes information such as the name, path, and type of the deployment space. type DeploymentSpaceOptions struct { - Name string `json:"name"` - Path string `json:"path"` - Type string `json:"type"` + Name string `json:"name"` // The name of the deployment space. + Path string `json:"path"` // The API path to access the deployment space. + Type string `json:"type"` // The type of the deployment space. } +// DeploymentOption represents a list of deployment options and information about the next page, if available. type DeploymentOption struct { - Option []map[string]any `json:"options"` - Next *NextPage `json:"nextPage"` + Option []map[string]any `json:"options"` // A slice of options for deployment. + Next *NextPage `json:"nextPage"` // Information about the next page of options. } +// NextPage provides details about the subsequent page of deployment options. +// +// It includes the name, path, and query parameters for accessing the next page. type NextPage struct { - Name string `json:"name"` - Path string `json:"path"` - Params map[string]string `json:"params"` + Name string `json:"name"` // The name of the next page. + Path string `json:"path"` // The API path to access the next page. + Params map[string]string `json:"params"` // Query parameters required for the next page. } +// apiResponse represents the structure of the API response for deployment options. +// +// It contains a data field with deployment options. type apiResponse struct { - Data *DeploymentOption `json:"data"` + Data *DeploymentOption `json:"data"` // The deployment option data. } From 705750da4a7d30f5be2d848627abed7d3bc47007 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 18 Dec 2024 13:08:45 +0530 Subject: [PATCH 23/26] add utils godoc --- utils/list.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/list.go b/utils/list.go index 4318e08..6ae9588 100644 --- a/utils/list.go +++ b/utils/list.go @@ -1,3 +1,6 @@ +// Package utils package provides utility functions for the application. +// It provides rendering of list based on the list of items provided. +// It also provides a function to unmarshal the response from the API to the struct. package utils import ( From 883caae17d456522b9b381070b4f0a4baaa7275d Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 18 Dec 2024 13:48:24 +0530 Subject: [PATCH 24/26] fix deployment Option models and infinite for loop --- deploymentspace/service/deployment.go | 2 ++ deploymentspace/service/models.go | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/deploymentspace/service/deployment.go b/deploymentspace/service/deployment.go index 66750ad..f10fbb5 100644 --- a/deploymentspace/service/deployment.go +++ b/deploymentspace/service/deployment.go @@ -128,6 +128,8 @@ func processOptions(ctx *gofr.Context, request map[string]any, path string) erro return ErrConnectingZopAPI } + option.Data.Next = nil + er = utils.GetResponse(resp, &option) if er != nil { ctx.Logger.Errorf("error fetching deployment options! %v", er) diff --git a/deploymentspace/service/models.go b/deploymentspace/service/models.go index 8df3d5d..6d6b5fe 100644 --- a/deploymentspace/service/models.go +++ b/deploymentspace/service/models.go @@ -11,14 +11,14 @@ type DeploymentSpaceOptions struct { // DeploymentOption represents a list of deployment options and information about the next page, if available. type DeploymentOption struct { - Option []map[string]any `json:"options"` // A slice of options for deployment. - Next *NextPage `json:"nextPage"` // Information about the next page of options. + Option []map[string]any `json:"options"` // A slice of options for deployment. + Next *Next `json:"next"` // Information about the next page of options. } -// NextPage provides details about the subsequent page of deployment options. +// Next provides details about the subsequent page of deployment options. // // It includes the name, path, and query parameters for accessing the next page. -type NextPage struct { +type Next struct { Name string `json:"name"` // The name of the next page. Path string `json:"path"` // The API path to access the next page. Params map[string]string `json:"params"` // Query parameters required for the next page. From e157e9f3442b6f987d6e71e6665793670d4bc40b Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 18 Dec 2024 14:06:59 +0530 Subject: [PATCH 25/26] add error check for POST deploymentSpace and option metadata in table --- deploymentspace/service/deployment.go | 24 +++++++++++++++++++++--- deploymentspace/service/errors.go | 10 ++++++++++ deploymentspace/service/listings.go | 4 ++-- deploymentspace/service/models.go | 9 +++++++-- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/deploymentspace/service/deployment.go b/deploymentspace/service/deployment.go index f10fbb5..40694cb 100644 --- a/deploymentspace/service/deployment.go +++ b/deploymentspace/service/deployment.go @@ -23,6 +23,9 @@ var ( // ErrorFetchingEnvironments is returned when there is an error fetching environments for a given application. ErrorFetchingEnvironments = errors.New("unable to fetch environments") + + // ErrUnknown is returned when an unknown error occurs while processing the request. + ErrUnknown = errors.New("unknown error occurred while processing the request") ) // Service represents the core service that handles cloud account and environment-related operations. @@ -91,6 +94,8 @@ func (s *Service) Add(ctx *gofr.Context) error { } func processOptions(ctx *gofr.Context, request map[string]any, path string) error { + var optionName string + api := ctx.GetHTTPService("api-service") resp, err := api.Get(ctx, path[1:], nil) @@ -109,7 +114,13 @@ func processOptions(ctx *gofr.Context, request map[string]any, path string) erro resp.Body.Close() for { - opt, er := getSelectedOption(ctx, option.Data.Option) + optionName = "option" + + if option.Data.Metadata != nil { + optionName = option.Data.Metadata.Name + } + + opt, er := getSelectedOption(ctx, option.Data.Option, optionName) if er != nil { return er } @@ -128,7 +139,7 @@ func processOptions(ctx *gofr.Context, request map[string]any, path string) erro return ErrConnectingZopAPI } - option.Data.Next = nil + option.Data = nil er = utils.GetResponse(resp, &option) if er != nil { @@ -175,7 +186,14 @@ func submitDeployment(ctx *gofr.Context, envID int64, request map[string]any) er } if resp.StatusCode != http.StatusCreated { - return ErrGettingDeploymentOptions + var er ErrorResponse + + err = utils.GetResponse(resp, &er) + if err != nil { + return ErrUnknown + } + + return &er } return resp.Body.Close() diff --git a/deploymentspace/service/errors.go b/deploymentspace/service/errors.go index 9e2c6ce..54975e0 100644 --- a/deploymentspace/service/errors.go +++ b/deploymentspace/service/errors.go @@ -16,3 +16,13 @@ type ErrNoItemSelected struct { func (e *ErrNoItemSelected) Error() string { return fmt.Sprintf("no %s selected", e.Type) } + +type ErrorResponse struct { + Er struct { + Message string `json:"message"` + } `json:"error"` +} + +func (e *ErrorResponse) Error() string { + return e.Er.Message +} diff --git a/deploymentspace/service/listings.go b/deploymentspace/service/listings.go index de5b2e5..807c1b3 100644 --- a/deploymentspace/service/listings.go +++ b/deploymentspace/service/listings.go @@ -119,7 +119,7 @@ func getDeploymentSpaceOptions(ctx *gofr.Context, id int64) (*DeploymentSpaceOpt return choice.Data.(*DeploymentSpaceOptions), nil } -func getSelectedOption(ctx *gofr.Context, items []map[string]any) (map[string]any, error) { +func getSelectedOption(ctx *gofr.Context, items []map[string]any, name string) (map[string]any, error) { listI := make([]*utils.Item, 0) if len(items) == 0 { @@ -130,7 +130,7 @@ func getSelectedOption(ctx *gofr.Context, items []map[string]any) (map[string]an listI = append(listI, &utils.Item{Name: item["name"].(string), Data: item}) } - choice, err := utils.RenderList("Select the option", listI) + choice, err := utils.RenderList("Select the "+name, listI) if err != nil { ctx.Logger.Errorf("unable to render the list of environments! %v", err) diff --git a/deploymentspace/service/models.go b/deploymentspace/service/models.go index 6d6b5fe..4affacd 100644 --- a/deploymentspace/service/models.go +++ b/deploymentspace/service/models.go @@ -11,8 +11,13 @@ type DeploymentSpaceOptions struct { // DeploymentOption represents a list of deployment options and information about the next page, if available. type DeploymentOption struct { - Option []map[string]any `json:"options"` // A slice of options for deployment. - Next *Next `json:"next"` // Information about the next page of options. + Option []map[string]any `json:"options"` // A slice of options for deployment. + Next *Next `json:"next"` // Information about the next page of options. + Metadata *metadata `json:"metadata"` // Additional metadata about the deployment options. +} + +type metadata struct { + Name string `json:"name"` } // Next provides details about the subsequent page of deployment options. From 424a9c5d1ae97a688dc2da14b6baa18e00b0d500 Mon Sep 17 00:00:00 2001 From: vipul-rawat Date: Wed, 18 Dec 2024 14:14:04 +0530 Subject: [PATCH 26/26] add spinners to improve the TUI --- deploymentspace/service/deployment.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/deploymentspace/service/deployment.go b/deploymentspace/service/deployment.go index 40694cb..7c36ea0 100644 --- a/deploymentspace/service/deployment.go +++ b/deploymentspace/service/deployment.go @@ -9,6 +9,7 @@ import ( "strings" "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/cmd/terminal" "zop.dev/cli/zop/utils" ) @@ -94,9 +95,13 @@ func (s *Service) Add(ctx *gofr.Context) error { } func processOptions(ctx *gofr.Context, request map[string]any, path string) error { - var optionName string + var ( + optionName string + sp = terminal.NewDotSpinner(ctx.Out) + api = ctx.GetHTTPService("api-service") + ) - api := ctx.GetHTTPService("api-service") + sp.Spin(ctx) resp, err := api.Get(ctx, path[1:], nil) if err != nil { @@ -112,6 +117,7 @@ func processOptions(ctx *gofr.Context, request map[string]any, path string) erro } resp.Body.Close() + sp.Stop() for { optionName = "option" @@ -125,6 +131,8 @@ func processOptions(ctx *gofr.Context, request map[string]any, path string) erro return er } + sp.Spin(ctx) + updateRequestWithOption(request, opt) if option.Data.Next == nil { @@ -148,6 +156,7 @@ func processOptions(ctx *gofr.Context, request map[string]any, path string) erro } resp.Body.Close() + sp.Stop() } return nil