From 3f373a7f25bc317e076a8fb59d90c2781853567a Mon Sep 17 00:00:00 2001 From: kardolus Date: Wed, 6 Nov 2024 12:14:26 +0100 Subject: [PATCH] chore: order packages based on domain --- {client => api/client}/callermocks_test.go | 0 {client => api/client}/client.go | 50 ++++---- {client => api/client}/client_test.go | 120 +++++++++--------- {client => api/client}/historymocks_test.go | 2 +- {client => api/client}/timermocks_test.go | 0 {types => api}/completions.go | 2 +- {http => api/http}/http.go | 15 ++- {http => api/http}/http_test.go | 2 +- {types => api}/models.go | 2 +- cmd/chatgpt/main.go | 35 +++-- {utils => cmd/chatgpt/utils}/utils.go | 64 ++-------- {utils => cmd/chatgpt/utils}/utils_test.go | 43 +------ {types => config}/config.go | 2 +- {configmanager => config}/configmocks_test.go | 4 +- .../configmanager.go => config/manager.go | 28 ++-- .../manager_test.go | 37 +++--- config/store.go | 33 +++-- history/history.go | 91 +------------ history/historymocks_test.go | 2 +- history/manager.go | 94 ++++++++++++++ history/{history_test.go => manager_test.go} | 26 ++-- history/store.go | 21 ++- internal/utils/utils.go | 47 +++++++ internal/utils/utils_test.go | 60 +++++++++ scripts/run_test.sh | 7 +- .../contract}/contract_test.go | 29 ++--- .../integration}/helpers_test.go | 13 +- .../integration}/integration_test.go | 42 +++--- utils/testutils.go => test/utils.go | 2 +- types/history.go | 8 -- 30 files changed, 449 insertions(+), 432 deletions(-) rename {client => api/client}/callermocks_test.go (100%) rename {client => api/client}/client.go (90%) rename {client => api/client}/client_test.go (86%) rename {client => api/client}/historymocks_test.go (98%) rename {client => api/client}/timermocks_test.go (100%) rename {types => api}/completions.go (99%) rename {http => api/http}/http.go (92%) rename {http => api/http}/http_test.go (97%) rename {types => api}/models.go (95%) rename {utils => cmd/chatgpt/utils}/utils.go (51%) rename {utils => cmd/chatgpt/utils}/utils_test.go (64%) rename {types => config}/config.go (98%) rename {configmanager => config}/configmocks_test.go (97%) rename configmanager/configmanager.go => config/manager.go (79%) rename configmanager/configmanager_test.go => config/manager_test.go (94%) create mode 100644 history/manager.go rename history/{history_test.go => manager_test.go} (76%) create mode 100644 internal/utils/utils.go create mode 100644 internal/utils/utils_test.go rename {integration => test/contract}/contract_test.go (84%) rename {integration => test/integration}/helpers_test.go (91%) rename {integration => test/integration}/integration_test.go (97%) rename utils/testutils.go => test/utils.go (97%) delete mode 100644 types/history.go diff --git a/client/callermocks_test.go b/api/client/callermocks_test.go similarity index 100% rename from client/callermocks_test.go rename to api/client/callermocks_test.go diff --git a/client/client.go b/api/client/client.go similarity index 90% rename from client/client.go rename to api/client/client.go index a9c1779..ee79a2a 100644 --- a/client/client.go +++ b/api/client/client.go @@ -4,14 +4,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/kardolus/chatgpt-cli/utils" + "github.com/google/uuid" + "github.com/kardolus/chatgpt-cli/api" + "github.com/kardolus/chatgpt-cli/api/http" + "github.com/kardolus/chatgpt-cli/config" "strings" "time" "unicode/utf8" "github.com/kardolus/chatgpt-cli/history" - "github.com/kardolus/chatgpt-cli/http" - "github.com/kardolus/chatgpt-cli/types" ) const ( @@ -36,18 +37,18 @@ func (r *RealTime) Now() time.Time { } type Client struct { - Config types.Config - History []types.History + Config config.Config + History []history.History caller http.Caller historyStore history.HistoryStore timer Timer } -func New(callerFactory http.CallerFactory, hs history.HistoryStore, t Timer, cfg types.Config, interactiveMode bool) *Client { +func New(callerFactory http.CallerFactory, hs history.HistoryStore, t Timer, cfg config.Config, interactiveMode bool) *Client { caller := callerFactory(cfg) if interactiveMode && cfg.AutoCreateNewThread { - hs.SetThread(utils.GenerateUniqueSlug(InteractiveThreadPrefix)) + hs.SetThread(GenerateUniqueSlug(InteractiveThreadPrefix)) } else { hs.SetThread(cfg.Thread) } @@ -94,7 +95,7 @@ func (c *Client) ListModels() ([]string, error) { return nil, err } - var response types.ListModelsResponse + var response api.ListModelsResponse if err := c.processResponse(raw, &response); err != nil { return nil, err } @@ -152,7 +153,7 @@ func (c *Client) Query(input string) (string, int, error) { return "", 0, err } - var response types.CompletionsResponse + var response api.CompletionsResponse if err := c.processResponse(raw, &response); err != nil { return "", 0, err } @@ -196,13 +197,13 @@ func (c *Client) Stream(input string) error { } func (c *Client) createBody(stream bool) ([]byte, error) { - var messages []types.Message + var messages []api.Message for _, item := range c.History { messages = append(messages, item.Message) } - body := types.CompletionsRequest{ + body := api.CompletionsRequest{ Messages: messages, Model: c.Config.Model, MaxTokens: c.Config.MaxTokens, @@ -227,8 +228,8 @@ func (c *Client) initHistory() { } if len(c.History) == 0 { - c.History = []types.History{{ - Message: types.Message{ + c.History = []history.History{{ + Message: api.Message{ Role: SystemRole, }, Timestamp: c.timer.Now(), @@ -239,12 +240,12 @@ func (c *Client) initHistory() { } func (c *Client) addQuery(query string) { - message := types.Message{ + message := api.Message{ Role: UserRole, Content: query, } - c.History = append(c.History, types.History{ + c.History = append(c.History, history.History{ Message: message, Timestamp: c.timer.Now(), }) @@ -296,8 +297,8 @@ func (c *Client) truncateHistory() { } func (c *Client) updateHistory(response string) { - c.History = append(c.History, types.History{ - Message: types.Message{ + c.History = append(c.History, history.History{ + Message: api.Message{ Role: AssistantRole, Content: response, }, @@ -315,7 +316,7 @@ func calculateEffectiveContextWindow(window int, bufferPercentage int) int { return effectiveContextWindow } -func countTokens(entries []types.History) (int, []int) { +func countTokens(entries []history.History) (int, []int) { var result int var rolling []int @@ -338,8 +339,8 @@ func countTokens(entries []types.History) (int, []int) { return result, rolling } -func (c *Client) createHistoryEntriesFromString(input string) []types.History { - var result []types.History +func (c *Client) createHistoryEntriesFromString(input string) []history.History { + var result []history.History words := strings.Fields(input) @@ -351,8 +352,8 @@ func (c *Client) createHistoryEntriesFromString(input string) []types.History { content := strings.Join(words[i:end], " ") - item := types.History{ - Message: types.Message{ + item := history.History{ + Message: api.Message{ Role: UserRole, Content: content, }, @@ -385,3 +386,8 @@ func (c *Client) printResponseDebugInfo(raw []byte) { fmt.Printf("\nResponse\n\n") fmt.Printf("%s\n\n", raw) } + +func GenerateUniqueSlug(prefix string) string { + guid := uuid.New() + return prefix + guid.String()[:4] +} diff --git a/client/client_test.go b/api/client/client_test.go similarity index 86% rename from client/client_test.go rename to api/client/client_test.go index 6402c22..48e3255 100644 --- a/client/client_test.go +++ b/api/client/client_test.go @@ -5,10 +5,12 @@ import ( "errors" "github.com/golang/mock/gomock" _ "github.com/golang/mock/mockgen/model" - "github.com/kardolus/chatgpt-cli/client" - "github.com/kardolus/chatgpt-cli/http" - "github.com/kardolus/chatgpt-cli/types" - "github.com/kardolus/chatgpt-cli/utils" + "github.com/kardolus/chatgpt-cli/api" + "github.com/kardolus/chatgpt-cli/api/client" + "github.com/kardolus/chatgpt-cli/api/http" + config2 "github.com/kardolus/chatgpt-cli/config" + "github.com/kardolus/chatgpt-cli/history" + "github.com/kardolus/chatgpt-cli/test" "os" "strings" "testing" @@ -36,7 +38,7 @@ var ( mockTimer *MockTimer factory *clientFactory apiKeyEnvVar string - config types.Config + config config2.Config ) func TestUnitClient(t *testing.T) { @@ -104,7 +106,7 @@ func testClient(t *testing.T, when spec.G, it spec.S) { when("Query()", func() { var ( body []byte - messages []types.Message + messages []api.Message err error ) @@ -140,12 +142,12 @@ func testClient(t *testing.T, when spec.G, it spec.S) { { description: "throws an error when the response is missing Choices", setupPostReturn: func() ([]byte, error) { - response := &types.CompletionsResponse{ + response := &api.CompletionsResponse{ ID: "id", Object: "object", Created: 0, Model: "model", - Choices: []types.Choice{}, + Choices: []api.Choice{}, } respBytes, err := json.Marshal(response) @@ -184,21 +186,21 @@ func testClient(t *testing.T, when spec.G, it spec.S) { tokens = 789 ) - choice := types.Choice{ - Message: types.Message{ + choice := api.Choice{ + Message: api.Message{ Role: client.AssistantRole, Content: answer, }, FinishReason: "", Index: 0, } - response := &types.CompletionsResponse{ + response := &api.CompletionsResponse{ ID: "id", Object: "object", Created: 0, Model: subject.Config.Model, - Choices: []types.Choice{choice}, - Usage: types.Usage{ + Choices: []api.Choice{choice}, + Usage: api.Usage{ PromptTokens: 123, CompletionTokens: 456, TotalTokens: tokens, @@ -209,22 +211,22 @@ func testClient(t *testing.T, when spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) mockCaller.EXPECT().Post(subject.Config.URL+subject.Config.CompletionsPath, expectedBody, false).Return(respBytes, nil) - var request types.CompletionsRequest + var request api.CompletionsRequest err = json.Unmarshal(expectedBody, &request) Expect(err).NotTo(HaveOccurred()) mockTimer.EXPECT().Now().Return(time.Time{}).AnyTimes() - var history []types.History + var h []history.History if !omitHistory { for _, msg := range request.Messages { - history = append(history, types.History{ + h = append(h, history.History{ Message: msg, }) } - mockHistoryStore.EXPECT().Write(append(history, types.History{ - Message: types.Message{ + mockHistoryStore.EXPECT().Write(append(h, history.History{ + Message: api.Message{ Role: client.AssistantRole, Content: answer, }, @@ -237,29 +239,29 @@ func testClient(t *testing.T, when spec.G, it spec.S) { Expect(usage).To(Equal(tokens)) } it("returns the expected result for a non-empty history", func() { - history := []types.History{ + h := []history.History{ { - Message: types.Message{ + Message: api.Message{ Role: client.SystemRole, Content: config.Role, }, }, { - Message: types.Message{ + Message: api.Message{ Role: client.UserRole, Content: "question 1", }, }, { - Message: types.Message{ + Message: api.Message{ Role: client.AssistantRole, Content: "answer 1", }, }, } - messages = createMessages(history, query) - factory.withHistory(history) + messages = createMessages(h, query) + factory.withHistory(h) subject := factory.buildClientWithoutConfig() body, err = createBody(messages, false) @@ -288,51 +290,51 @@ func testClient(t *testing.T, when spec.G, it spec.S) { testValidHTTPResponse(subject, body, true) }) it("truncates the history as expected", func() { - history := []types.History{ + hs := []history.History{ { - Message: types.Message{ + Message: api.Message{ Role: client.SystemRole, Content: config.Role, }, Timestamp: time.Time{}, }, { - Message: types.Message{ + Message: api.Message{ Role: client.UserRole, Content: "question 1", }, Timestamp: time.Time{}, }, { - Message: types.Message{ + Message: api.Message{ Role: client.AssistantRole, Content: "answer 1", }, Timestamp: time.Time{}, }, { - Message: types.Message{ + Message: api.Message{ Role: client.UserRole, Content: "question 2", }, Timestamp: time.Time{}, }, { - Message: types.Message{ + Message: api.Message{ Role: client.AssistantRole, Content: "answer 2", }, Timestamp: time.Time{}, }, { - Message: types.Message{ + Message: api.Message{ Role: client.UserRole, Content: "question 3", }, Timestamp: time.Time{}, }, { - Message: types.Message{ + Message: api.Message{ Role: client.AssistantRole, Content: "answer 3", }, @@ -340,9 +342,9 @@ func testClient(t *testing.T, when spec.G, it spec.S) { }, } - messages = createMessages(history, query) + messages = createMessages(hs, query) - factory.withHistory(history) + factory.withHistory(hs) subject := factory.buildClientWithoutConfig() // messages get truncated. Index 1+2 are cut out @@ -358,7 +360,7 @@ func testClient(t *testing.T, when spec.G, it spec.S) { when("Stream()", func() { var ( body []byte - messages []types.Message + messages []api.Message err error ) @@ -382,7 +384,7 @@ func testClient(t *testing.T, when spec.G, it spec.S) { when("a valid http response is received", func() { const answer = "answer" - testValidHTTPResponse := func(subject *client.Client, history []types.History, expectedBody []byte) { + testValidHTTPResponse := func(subject *client.Client, hs []history.History, expectedBody []byte) { messages = createMessages(nil, query) body, err = createBody(messages, true) Expect(err).NotTo(HaveOccurred()) @@ -391,18 +393,18 @@ func testClient(t *testing.T, when spec.G, it spec.S) { mockTimer.EXPECT().Now().Return(time.Time{}).AnyTimes() - messages = createMessages(history, query) + messages = createMessages(hs, query) - history = []types.History{} + hs = []history.History{} for _, message := range messages { - history = append(history, types.History{ + hs = append(hs, history.History{ Message: message, }) } - mockHistoryStore.EXPECT().Write(append(history, types.History{ - Message: types.Message{ + mockHistoryStore.EXPECT().Write(append(hs, history.History{ + Message: api.Message{ Role: client.AssistantRole, Content: answer, }, @@ -423,34 +425,34 @@ func testClient(t *testing.T, when spec.G, it spec.S) { testValidHTTPResponse(subject, nil, body) }) it("returns the expected result for a non-empty history", func() { - history := []types.History{ + h := []history.History{ { - Message: types.Message{ + Message: api.Message{ Role: client.SystemRole, Content: config.Role, }, }, { - Message: types.Message{ + Message: api.Message{ Role: client.UserRole, Content: "question x", }, }, { - Message: types.Message{ + Message: api.Message{ Role: client.AssistantRole, Content: "answer x", }, }, } - factory.withHistory(history) + factory.withHistory(h) subject := factory.buildClientWithoutConfig() - messages = createMessages(history, query) + messages = createMessages(h, query) body, err = createBody(messages, true) Expect(err).NotTo(HaveOccurred()) - testValidHTTPResponse(subject, history, body) + testValidHTTPResponse(subject, h, body) }) }) }) @@ -487,7 +489,7 @@ func testClient(t *testing.T, when spec.G, it spec.S) { it("filters gpt models as expected", func() { subject := factory.buildClientWithoutConfig() - response, err := utils.FileToBytes("models.json") + response, err := test.FileToBytes("models.json") Expect(err).NotTo(HaveOccurred()) mockCaller.EXPECT().Get(subject.Config.URL+subject.Config.ModelsPath).Return(response, nil) @@ -524,8 +526,8 @@ func testClient(t *testing.T, when spec.G, it spec.S) { }) } -func createBody(messages []types.Message, stream bool) ([]byte, error) { - req := types.CompletionsRequest{ +func createBody(messages []api.Message, stream bool) ([]byte, error) { + req := api.CompletionsRequest{ Model: config.Model, Messages: messages, Stream: stream, @@ -540,11 +542,11 @@ func createBody(messages []types.Message, stream bool) ([]byte, error) { return json.Marshal(req) } -func createMessages(historyEntries []types.History, query string) []types.Message { - var messages []types.Message +func createMessages(historyEntries []history.History, query string) []api.Message { + var messages []api.Message if len(historyEntries) == 0 { - messages = append(messages, types.Message{ + messages = append(messages, api.Message{ Role: client.SystemRole, Content: config.Role, }) @@ -554,7 +556,7 @@ func createMessages(historyEntries []types.History, query string) []types.Messag } } - messages = append(messages, types.Message{ + messages = append(messages, api.Message{ Role: client.UserRole, Content: query, }) @@ -584,16 +586,16 @@ func (f *clientFactory) withoutHistory() { f.mockHistoryStore.EXPECT().Read().Return(nil, nil).Times(1) } -func (f *clientFactory) withHistory(history []types.History) { +func (f *clientFactory) withHistory(history []history.History) { f.mockHistoryStore.EXPECT().Read().Return(history, nil).Times(1) } -func mockCallerFactory(_ types.Config) http.Caller { +func mockCallerFactory(_ config2.Config) http.Caller { return mockCaller } -func MockConfig() types.Config { - return types.Config{ +func MockConfig() config2.Config { + return config2.Config{ Name: "mock-openai", APIKey: "mock-api-key", Model: "gpt-3.5-turbo", diff --git a/client/historymocks_test.go b/api/client/historymocks_test.go similarity index 98% rename from client/historymocks_test.go rename to api/client/historymocks_test.go index 94a7f58..c14f6bb 100644 --- a/client/historymocks_test.go +++ b/api/client/historymocks_test.go @@ -5,10 +5,10 @@ package client_test import ( + types "github.com/kardolus/chatgpt-cli/history" reflect "reflect" gomock "github.com/golang/mock/gomock" - types "github.com/kardolus/chatgpt-cli/types" ) // MockHistoryStore is a mock of HistoryStore interface. diff --git a/client/timermocks_test.go b/api/client/timermocks_test.go similarity index 100% rename from client/timermocks_test.go rename to api/client/timermocks_test.go diff --git a/types/completions.go b/api/completions.go similarity index 99% rename from types/completions.go rename to api/completions.go index 73898a1..a2549a7 100644 --- a/types/completions.go +++ b/api/completions.go @@ -1,4 +1,4 @@ -package types +package api import "encoding/json" diff --git a/http/http.go b/api/http/http.go similarity index 92% rename from http/http.go rename to api/http/http.go index 76bc114..07c9c0c 100644 --- a/http/http.go +++ b/api/http/http.go @@ -6,7 +6,8 @@ import ( "crypto/tls" "encoding/json" "fmt" - "github.com/kardolus/chatgpt-cli/types" + "github.com/kardolus/chatgpt-cli/api" + "github.com/kardolus/chatgpt-cli/config" "io" "net/http" "os" @@ -30,13 +31,13 @@ type Caller interface { type RestCaller struct { client *http.Client - config types.Config + config config.Config } // Ensure RestCaller implements Caller interface var _ Caller = &RestCaller{} -func New(cfg types.Config) *RestCaller { +func New(cfg config.Config) *RestCaller { var client *http.Client if cfg.SkipTLSVerify { transport := &http.Transport{ @@ -55,9 +56,9 @@ func New(cfg types.Config) *RestCaller { } } -type CallerFactory func(cfg types.Config) Caller +type CallerFactory func(cfg config.Config) Caller -func RealCallerFactory(cfg types.Config) Caller { +func RealCallerFactory(cfg config.Config) Caller { return New(cfg) } @@ -97,7 +98,7 @@ func (r *RestCaller) ProcessResponse(reader io.Reader, writer io.Writer) []byte break } - var data types.Data + var data api.Data err := json.Unmarshal([]byte(line), &data) if err != nil { _, _ = fmt.Fprintf(writer, "Error: %s\n", err.Error()) @@ -133,7 +134,7 @@ func (r *RestCaller) doRequest(method, url string, body []byte, stream bool) ([] return nil, fmt.Errorf(errHTTPStatus, response.StatusCode) } - var errorData types.ErrorResponse + var errorData api.ErrorResponse if err := json.Unmarshal(errorResponse, &errorData); err != nil { return nil, fmt.Errorf(errHTTPStatus, response.StatusCode) } diff --git a/http/http_test.go b/api/http/http_test.go similarity index 97% rename from http/http_test.go rename to api/http/http_test.go index dd91a7f..a14495d 100644 --- a/http/http_test.go +++ b/api/http/http_test.go @@ -2,7 +2,7 @@ package http_test import ( "bytes" - "github.com/kardolus/chatgpt-cli/http" + "github.com/kardolus/chatgpt-cli/api/http" "strings" "testing" diff --git a/types/models.go b/api/models.go similarity index 95% rename from types/models.go rename to api/models.go index 4ceec0f..08a0e26 100644 --- a/types/models.go +++ b/api/models.go @@ -1,4 +1,4 @@ -package types +package api type ListModelsResponse struct { Object string `json:"object"` diff --git a/cmd/chatgpt/main.go b/cmd/chatgpt/main.go index 0c3ed21..ac24dfa 100644 --- a/cmd/chatgpt/main.go +++ b/cmd/chatgpt/main.go @@ -3,8 +3,10 @@ package main import ( "errors" "fmt" - "github.com/kardolus/chatgpt-cli/types" - "github.com/kardolus/chatgpt-cli/utils" + "github.com/kardolus/chatgpt-cli/api/client" + "github.com/kardolus/chatgpt-cli/api/http" + utils2 "github.com/kardolus/chatgpt-cli/cmd/chatgpt/utils" + "github.com/kardolus/chatgpt-cli/internal/utils" "github.com/spf13/pflag" "gopkg.in/yaml.v3" "io" @@ -13,11 +15,8 @@ import ( "time" "github.com/chzyer/readline" - "github.com/kardolus/chatgpt-cli/client" "github.com/kardolus/chatgpt-cli/config" - "github.com/kardolus/chatgpt-cli/configmanager" "github.com/kardolus/chatgpt-cli/history" - "github.com/kardolus/chatgpt-cli/http" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -39,7 +38,7 @@ var ( threadName string ServiceURL string shell string - cfg types.Config + cfg config.Config ) type ConfigMetadata struct { @@ -139,7 +138,7 @@ func run(cmd *cobra.Command, args []string) error { } if cmd.Flag("delete-thread").Changed { - cm := configmanager.New(config.New()) + cm := config.NewManager(config.NewStore()) if err := cm.DeleteThread(threadName); err != nil { return err @@ -149,7 +148,7 @@ func run(cmd *cobra.Command, args []string) error { } if listThreads { - cm := configmanager.New(config.New()) + cm := config.NewManager(config.NewStore()) threads, err := cm.ListThreads() if err != nil { @@ -163,7 +162,7 @@ func run(cmd *cobra.Command, args []string) error { } if clearHistory { - cm := configmanager.New(config.New()) + cm := config.NewManager(config.NewStore()) if err := cm.DeleteThread(cfg.Thread); err != nil { return err @@ -221,7 +220,7 @@ func run(cmd *cobra.Command, args []string) error { } if hs != nil && newThread { - slug := utils.GenerateUniqueSlug("cmd_") + slug := client.GenerateUniqueSlug("cmd_") hs.SetThread(slug) @@ -231,7 +230,7 @@ func run(cmd *cobra.Command, args []string) error { } if cmd.Flag("prompt").Changed { - prompt, err := utils.FileToString(promptFile) + prompt, err := utils2.FileToString(promptFile) if err != nil { return err } @@ -279,7 +278,7 @@ func run(cmd *cobra.Command, args []string) error { defer rl.Close() commandPrompt := func(counter, usage int) string { - return utils.FormatPrompt(c.Config.CommandPrompt, counter, usage, time.Now()) + return utils2.FormatPrompt(c.Config.CommandPrompt, counter, usage, time.Now()) } qNum, usage := 1, 0 @@ -292,7 +291,7 @@ func run(cmd *cobra.Command, args []string) error { return nil } - fmtOutputPrompt := utils.FormatPrompt(c.Config.OutputPrompt, qNum, usage, time.Now()) + fmtOutputPrompt := utils2.FormatPrompt(c.Config.OutputPrompt, qNum, usage, time.Now()) if queryMode { result, qUsage, err := c.Query(input) @@ -336,14 +335,14 @@ func run(cmd *cobra.Command, args []string) error { return nil } -func initConfig(rootCmd *cobra.Command) (types.Config, error) { +func initConfig(rootCmd *cobra.Command) (config.Config, error) { // Set default name for environment variables if no config is loaded yet. viper.SetDefault("name", "openai") // Read only the `name` field from the config to determine the environment prefix. configHome, err := utils.GetConfigHome() if err != nil { - return types.Config{}, err + return config.Config{}, err } viper.SetConfigName("config") viper.SetConfigType("yaml") @@ -353,7 +352,7 @@ func initConfig(rootCmd *cobra.Command) (types.Config, error) { if err := viper.ReadInConfig(); err != nil { var configFileNotFoundError viper.ConfigFileNotFoundError if !errors.As(err, &configFileNotFoundError) { - return types.Config{}, err + return config.Config{}, err } } @@ -701,8 +700,8 @@ func syncFlag(cmd *cobra.Command, meta ConfigMetadata, alias string) error { return nil } -func createConfigFromViper() types.Config { - return types.Config{ +func createConfigFromViper() config.Config { + return config.Config{ Name: viper.GetString("name"), APIKey: viper.GetString("api_key"), Model: viper.GetString("model"), diff --git a/utils/utils.go b/cmd/chatgpt/utils/utils.go similarity index 51% rename from utils/utils.go rename to cmd/chatgpt/utils/utils.go index 88b3e72..6473982 100644 --- a/utils/utils.go +++ b/cmd/chatgpt/utils/utils.go @@ -2,19 +2,19 @@ package utils import ( "fmt" - "github.com/google/uuid" "os" - "path/filepath" "strings" "time" ) -const ( - ConfigHomeEnv = "OPENAI_CONFIG_HOME" - DataHomeEnv = "OPENAI_DATA_HOME" - DefaultConfigDir = ".chatgpt-cli" - DefaultDataDir = "history" -) +func FileToString(fileName string) (string, error) { + bytes, err := os.ReadFile(fileName) + if err != nil { + return "", err + } + + return string(bytes), nil +} func FormatPrompt(str string, counter, usage int, now time.Time) string { variables := map[string]string{ @@ -39,51 +39,3 @@ func FormatPrompt(str string, counter, usage int, now time.Time) string { return str } - -func GetConfigHome() (string, error) { - var result string - - homeDir, err := os.UserHomeDir() - if err != nil { - return "", err - } - - result = filepath.Join(homeDir, DefaultConfigDir) - - if tmp := os.Getenv(ConfigHomeEnv); tmp != "" { - result = tmp - } - - return result, nil -} - -func GetDataHome() (string, error) { - var result string - - configHome, err := GetConfigHome() - if err != nil { - return "", err - } - - result = filepath.Join(configHome, DefaultDataDir) - - if tmp := os.Getenv(DataHomeEnv); tmp != "" { - result = tmp - } - - return result, nil -} - -func GenerateUniqueSlug(prefix string) string { - guid := uuid.New() - return prefix + guid.String()[:4] -} - -func FileToString(fileName string) (string, error) { - bytes, err := os.ReadFile(fileName) - if err != nil { - return "", err - } - - return string(bytes), nil -} diff --git a/utils/utils_test.go b/cmd/chatgpt/utils/utils_test.go similarity index 64% rename from utils/utils_test.go rename to cmd/chatgpt/utils/utils_test.go index b4b0439..5b1ad4f 100644 --- a/utils/utils_test.go +++ b/cmd/chatgpt/utils/utils_test.go @@ -2,8 +2,7 @@ package utils_test import ( "fmt" - "github.com/kardolus/chatgpt-cli/utils" - "os" + "github.com/kardolus/chatgpt-cli/cmd/chatgpt/utils" "testing" "time" @@ -19,8 +18,6 @@ func TestUnitUtils(t *testing.T) { func testUtils(t *testing.T, when spec.G, it spec.S) { it.Before(func() { RegisterTestingT(t) - Expect(os.Unsetenv(utils.ConfigHomeEnv)).To(Succeed()) - Expect(os.Unsetenv(utils.DataHomeEnv)).To(Succeed()) }) when("FormatPrompt()", func() { @@ -88,42 +85,4 @@ func testUtils(t *testing.T, when spec.G, it spec.S) { Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected)) }) }) - - when("GetConfigHome()", func() { - it("Uses the default value if OPENAI_CONFIG_HOME is not set", func() { - configHome, err := utils.GetConfigHome() - - Expect(err).NotTo(HaveOccurred()) - Expect(configHome).To(ContainSubstring(".chatgpt-cli")) // Assuming default location is ~/.chatgpt-cli - }) - - it("Overwrites the default when OPENAI_CONFIG_HOME is set", func() { - customConfigHome := "/custom/config/path" - Expect(os.Setenv("OPENAI_CONFIG_HOME", customConfigHome)).To(Succeed()) - - configHome, err := utils.GetConfigHome() - - Expect(err).NotTo(HaveOccurred()) - Expect(configHome).To(Equal(customConfigHome)) - }) - }) - - when("GetDataHome()", func() { - it("Uses the default value if OPENAI_DATA_HOME is not set", func() { - dataHome, err := utils.GetDataHome() - - Expect(err).NotTo(HaveOccurred()) - Expect(dataHome).To(ContainSubstring(".chatgpt-cli/history")) // Assuming default location is ~/.local/share/chatgpt-cli - }) - - it("Overwrites the default when OPENAI_DATA_HOME is set", func() { - customDataHome := "/custom/data/path" - Expect(os.Setenv("OPENAI_DATA_HOME", customDataHome)).To(Succeed()) - - dataHome, err := utils.GetDataHome() - - Expect(err).NotTo(HaveOccurred()) - Expect(dataHome).To(Equal(customDataHome)) - }) - }) } diff --git a/types/config.go b/config/config.go similarity index 98% rename from types/config.go rename to config/config.go index c77ae30..a77332a 100644 --- a/types/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package types +package config type Config struct { Name string `yaml:"name"` diff --git a/configmanager/configmocks_test.go b/config/configmocks_test.go similarity index 97% rename from configmanager/configmocks_test.go rename to config/configmocks_test.go index 85b3527..d46ee41 100644 --- a/configmanager/configmocks_test.go +++ b/config/configmocks_test.go @@ -2,13 +2,13 @@ // Source: github.com/kardolus/chatgpt-cli/config (interfaces: ConfigStore) // Package configmanager_test is a generated GoMock package. -package configmanager_test +package config_test import ( + types "github.com/kardolus/chatgpt-cli/config" reflect "reflect" gomock "github.com/golang/mock/gomock" - types "github.com/kardolus/chatgpt-cli/types" ) // MockConfigStore is a mock of ConfigStore interface. diff --git a/configmanager/configmanager.go b/config/manager.go similarity index 79% rename from configmanager/configmanager.go rename to config/manager.go index 476b1bf..bbc83ee 100644 --- a/configmanager/configmanager.go +++ b/config/manager.go @@ -1,9 +1,7 @@ -package configmanager +package config import ( "fmt" - "github.com/kardolus/chatgpt-cli/config" - "github.com/kardolus/chatgpt-cli/types" "gopkg.in/yaml.v3" "os" "reflect" @@ -11,12 +9,12 @@ import ( "strings" ) -type ConfigManager struct { - configStore config.ConfigStore - Config types.Config +type Manager struct { + configStore ConfigStore + Config Config } -func New(cs config.ConfigStore) *ConfigManager { +func NewManager(cs ConfigStore) *Manager { configuration := cs.ReadDefaults() userConfig, err := cs.Read() @@ -24,28 +22,28 @@ func New(cs config.ConfigStore) *ConfigManager { configuration = replaceByConfigFile(configuration, userConfig) } - return &ConfigManager{configStore: cs, Config: configuration} + return &Manager{configStore: cs, Config: configuration} } -func (c *ConfigManager) WithEnvironment() *ConfigManager { +func (c *Manager) WithEnvironment() *Manager { c.Config = replaceByEnvironment(c.Config) return c } -func (c *ConfigManager) APIKeyEnvVarName() string { +func (c *Manager) APIKeyEnvVarName() string { return strings.ToUpper(c.Config.Name) + "_" + "API_KEY" } // DeleteThread removes the specified thread from the configuration store. // This operation is idempotent; non-existent threads do not cause errors. -func (c *ConfigManager) DeleteThread(thread string) error { +func (c *Manager) DeleteThread(thread string) error { return c.configStore.Delete(thread) } // ListThreads retrieves a list of all threads stored in the configuration. // It marks the current thread with an asterisk (*) and returns the list sorted alphabetically. // If an error occurs while retrieving the threads from the config store, it returns the error. -func (c *ConfigManager) ListThreads() ([]string, error) { +func (c *Manager) ListThreads() ([]string, error) { var result []string threads, err := c.configStore.List() @@ -67,7 +65,7 @@ func (c *ConfigManager) ListThreads() ([]string, error) { // ShowConfig serializes the current configuration to a YAML string. // It returns the serialized string or an error if the serialization fails. -func (c *ConfigManager) ShowConfig() (string, error) { +func (c *Manager) ShowConfig() (string, error) { data, err := yaml.Marshal(c.Config) if err != nil { return "", err @@ -76,7 +74,7 @@ func (c *ConfigManager) ShowConfig() (string, error) { return string(data), nil } -func replaceByConfigFile(defaultConfig, userConfig types.Config) types.Config { +func replaceByConfigFile(defaultConfig, userConfig Config) Config { t := reflect.TypeOf(defaultConfig) vDefault := reflect.ValueOf(&defaultConfig).Elem() vUser := reflect.ValueOf(userConfig) @@ -106,7 +104,7 @@ func replaceByConfigFile(defaultConfig, userConfig types.Config) types.Config { return defaultConfig } -func replaceByEnvironment(configuration types.Config) types.Config { +func replaceByEnvironment(configuration Config) Config { t := reflect.TypeOf(configuration) v := reflect.ValueOf(&configuration).Elem() diff --git a/configmanager/configmanager_test.go b/config/manager_test.go similarity index 94% rename from configmanager/configmanager_test.go rename to config/manager_test.go index f1462b0..dfa7bb0 100644 --- a/configmanager/configmanager_test.go +++ b/config/manager_test.go @@ -1,14 +1,13 @@ -package configmanager_test +package config_test import ( "errors" + "github.com/kardolus/chatgpt-cli/config" "os" "strings" "testing" "github.com/golang/mock/gomock" - "github.com/kardolus/chatgpt-cli/configmanager" - "github.com/kardolus/chatgpt-cli/types" . "github.com/onsi/gomega" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -50,7 +49,7 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { var ( mockCtrl *gomock.Controller mockConfigStore *MockConfigStore - defaultConfig types.Config + defaultConfig config.Config envPrefix string ) @@ -59,7 +58,7 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { mockCtrl = gomock.NewController(t) mockConfigStore = NewMockConfigStore(mockCtrl) - defaultConfig = types.Config{ + defaultConfig = config.Config{ Name: defaultName, APIKey: defaultApiKey, Model: defaultModel, @@ -96,9 +95,9 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { it("should use default values when no environment variables or user config are provided", func() { mockConfigStore.EXPECT().ReadDefaults().Return(defaultConfig).Times(1) - mockConfigStore.EXPECT().Read().Return(types.Config{}, errors.New("no user config")).Times(1) + mockConfigStore.EXPECT().Read().Return(config.Config{}, errors.New("no user config")).Times(1) - subject := configmanager.New(mockConfigStore).WithEnvironment() + subject := config.NewManager(mockConfigStore).WithEnvironment() Expect(subject.Config.MaxTokens).To(Equal(defaultMaxTokens)) Expect(subject.Config.ContextWindow).To(Equal(defaultContextWindow)) @@ -125,7 +124,7 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { }) it("should prioritize user-provided config over defaults", func() { - userConfig := types.Config{ + userConfig := config.Config{ Model: "user-model", MaxTokens: 20, ContextWindow: 30, @@ -152,7 +151,7 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { mockConfigStore.EXPECT().ReadDefaults().Return(defaultConfig).Times(1) mockConfigStore.EXPECT().Read().Return(userConfig, nil).Times(1) - subject := configmanager.New(mockConfigStore).WithEnvironment() + subject := config.NewManager(mockConfigStore).WithEnvironment() Expect(subject.Config.Model).To(Equal("user-model")) Expect(subject.Config.MaxTokens).To(Equal(20)) @@ -202,9 +201,9 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { Expect(os.Setenv(envPrefix+"OUTPUT_PROMPT", "env-output-prompt")).To(Succeed()) mockConfigStore.EXPECT().ReadDefaults().Return(defaultConfig).Times(1) - mockConfigStore.EXPECT().Read().Return(types.Config{}, errors.New("config error")).Times(1) + mockConfigStore.EXPECT().Read().Return(config.Config{}, errors.New("config error")).Times(1) - subject := configmanager.New(mockConfigStore).WithEnvironment() + subject := config.NewManager(mockConfigStore).WithEnvironment() Expect(subject.Config.APIKey).To(Equal("env-api-key")) Expect(subject.Config.Model).To(Equal("env-model")) @@ -254,7 +253,7 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { Expect(os.Setenv(envPrefix+"COMMAND_PROMPT", "env-command-prompt")).To(Succeed()) Expect(os.Setenv(envPrefix+"OUTPUT_PROMPT", "env-output-prompt")).To(Succeed()) - userConfig := types.Config{ + userConfig := config.Config{ APIKey: "user-api-key", Model: "user-model", MaxTokens: 20, @@ -282,7 +281,7 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { mockConfigStore.EXPECT().ReadDefaults().Return(defaultConfig).Times(1) mockConfigStore.EXPECT().Read().Return(userConfig, nil).Times(1) - subject := configmanager.New(mockConfigStore).WithEnvironment() + subject := config.NewManager(mockConfigStore).WithEnvironment() Expect(subject.Config.APIKey).To(Equal("env-api-key")) Expect(subject.Config.Model).To(Equal("env-model")) @@ -309,17 +308,17 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { }) when("DeleteThread()", func() { - var subject *configmanager.ConfigManager + var subject *config.Manager threadName := "non-active-thread" it.Before(func() { - userConfig := types.Config{Thread: threadName} + userConfig := config.Config{Thread: threadName} mockConfigStore.EXPECT().ReadDefaults().Return(defaultConfig).Times(1) mockConfigStore.EXPECT().Read().Return(userConfig, nil).Times(1) - subject = configmanager.New(mockConfigStore).WithEnvironment() + subject = config.NewManager(mockConfigStore).WithEnvironment() }) it("propagates the error from the config store", func() { @@ -345,14 +344,14 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { activeThread := "active-thread" it.Before(func() { - userConfig := types.Config{Thread: activeThread} + userConfig := config.Config{Thread: activeThread} mockConfigStore.EXPECT().ReadDefaults().Return(defaultConfig).Times(1) mockConfigStore.EXPECT().Read().Return(userConfig, nil).Times(1) }) it("throws an error when the List call fails", func() { - subject := configmanager.New(mockConfigStore).WithEnvironment() + subject := config.NewManager(mockConfigStore).WithEnvironment() errorInstance := errors.New("an error occurred") mockConfigStore.EXPECT().List().Return(nil, errorInstance).Times(1) @@ -363,7 +362,7 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { }) it("returns the expected threads", func() { - subject := configmanager.New(mockConfigStore).WithEnvironment() + subject := config.NewManager(mockConfigStore).WithEnvironment() threads := []string{"thread1.json", "thread2.json", activeThread + ".json"} mockConfigStore.EXPECT().List().Return(threads, nil).Times(1) diff --git a/config/store.go b/config/store.go index fc4453d..c4863cd 100644 --- a/config/store.go +++ b/config/store.go @@ -1,8 +1,7 @@ package config import ( - "github.com/kardolus/chatgpt-cli/types" - "github.com/kardolus/chatgpt-cli/utils" + "github.com/kardolus/chatgpt-cli/internal/utils" "gopkg.in/yaml.v3" "os" "path/filepath" @@ -32,9 +31,9 @@ const ( type ConfigStore interface { Delete(string) error List() ([]string, error) - Read() (types.Config, error) - ReadDefaults() types.Config - Write(types.Config) error + Read() (Config, error) + ReadDefaults() Config + Write(Config) error } // Ensure FileIO implements ConfigStore interface @@ -45,7 +44,7 @@ type FileIO struct { historyFilePath string } -func New() *FileIO { +func NewStore() *FileIO { configPath, _ := getPath() historyPath, _ := utils.GetDataHome() @@ -89,17 +88,17 @@ func (f *FileIO) List() ([]string, error) { return result, nil } -func (f *FileIO) Read() (types.Config, error) { +func (f *FileIO) Read() (Config, error) { result, err := parseFile(f.configFilePath) if err != nil { - return types.Config{}, err + return Config{}, err } return migrate(result), nil } -func (f *FileIO) ReadDefaults() types.Config { - return types.Config{ +func (f *FileIO) ReadDefaults() Config { + return Config{ Name: openAIName, Model: openAIModel, Role: openAIRole, @@ -119,7 +118,7 @@ func (f *FileIO) ReadDefaults() types.Config { } } -func (f *FileIO) Write(config types.Config) error { +func (f *FileIO) Write(config Config) error { rootNode, err := f.readNode() // If readNode returns an error or there was a problem reading the rootNode, initialize a new rootNode. @@ -162,7 +161,7 @@ func getPath() (string, error) { return filepath.Join(homeDir, "config.yaml"), nil } -func migrate(config types.Config) types.Config { +func migrate(config Config) Config { // the "old" max_tokens became context_window if config.ContextWindow == 0 && config.MaxTokens > 0 { config.ContextWindow = config.MaxTokens @@ -175,16 +174,16 @@ func migrate(config types.Config) types.Config { return config } -func parseFile(fileName string) (types.Config, error) { - var result types.Config +func parseFile(fileName string) (Config, error) { + var result Config buf, err := os.ReadFile(fileName) if err != nil { - return types.Config{}, err + return Config{}, err } if err := yaml.Unmarshal(buf, &result); err != nil { - return types.Config{}, err + return Config{}, err } return result, nil @@ -192,7 +191,7 @@ func parseFile(fileName string) (types.Config, error) { // updateNodeFromConfig updates the specified yaml.Node with values from the Config struct. // It uses reflection to match struct fields with YAML tags, updating the node accordingly. -func updateNodeFromConfig(node *yaml.Node, config types.Config) { +func updateNodeFromConfig(node *yaml.Node, config Config) { t := reflect.TypeOf(config) v := reflect.ValueOf(config) diff --git a/history/history.go b/history/history.go index 38a437f..c15c4dc 100644 --- a/history/history.go +++ b/history/history.go @@ -1,94 +1,11 @@ package history import ( - "fmt" - "github.com/kardolus/chatgpt-cli/types" - "strings" -) - -const ( - assistantRole = "assistant" - systemRole = "system" - userRole = "user" + "github.com/kardolus/chatgpt-cli/api" + "time" ) type History struct { - store HistoryStore -} - -func NewHistory(store HistoryStore) *History { - return &History{store: store} -} - -func (h *History) Print(thread string) (string, error) { - var result string - - historyEntries, err := h.store.ReadThread(thread) - if err != nil { - return "", err - } - - var ( - lastRole string - concatenatedMessage string - ) - - for _, entry := range historyEntries { - if entry.Role == userRole && lastRole == userRole { - concatenatedMessage += entry.Content - } else { - if lastRole == userRole && concatenatedMessage != "" { - result += formatHistory(types.History{ - Message: types.Message{Role: userRole, Content: concatenatedMessage}, - Timestamp: entry.Timestamp, - }) - concatenatedMessage = "" - } - - if entry.Role == userRole { - concatenatedMessage = entry.Content - } else { - result += formatHistory(types.History{ - Message: entry.Message, - Timestamp: entry.Timestamp, - }) - } - } - - lastRole = entry.Role - } - - // Handle the case where the last entry is a user entry and was concatenated - if lastRole == userRole && concatenatedMessage != "" { - result += formatHistory(types.History{ - Message: types.Message{Role: userRole, Content: concatenatedMessage}, - }) - } - - return result, nil -} - -func formatHistory(entry types.History) string { - var ( - emoji string - prefix string - timestamp string - ) - - switch entry.Role { - case systemRole: - emoji = "💻" - prefix = "\n" - case userRole: - emoji = "👤" - prefix = "---\n" - if !entry.Timestamp.IsZero() { - timestamp = fmt.Sprintf(" [%s]", entry.Timestamp.Format("2006-01-02 15:04:05")) - } - case assistantRole: - emoji = "🤖" - prefix = "\n" - } - - return fmt.Sprintf("%s**%s** %s%s:\n%s\n", prefix, strings.ToUpper(entry.Role), emoji, timestamp, entry.Content) + api.Message + Timestamp time.Time `json:"timestamp,omitempty"` } diff --git a/history/historymocks_test.go b/history/historymocks_test.go index 8edf148..67782cc 100644 --- a/history/historymocks_test.go +++ b/history/historymocks_test.go @@ -5,10 +5,10 @@ package history_test import ( + types "github.com/kardolus/chatgpt-cli/history" reflect "reflect" gomock "github.com/golang/mock/gomock" - types "github.com/kardolus/chatgpt-cli/types" ) // MockHistoryStore is a mock of HistoryStore interface. diff --git a/history/manager.go b/history/manager.go new file mode 100644 index 0000000..f911d89 --- /dev/null +++ b/history/manager.go @@ -0,0 +1,94 @@ +package history + +import ( + "fmt" + "github.com/kardolus/chatgpt-cli/api" + "strings" +) + +const ( + assistantRole = "assistant" + systemRole = "system" + userRole = "user" +) + +type Manager struct { + store HistoryStore +} + +func NewHistory(store HistoryStore) *Manager { + return &Manager{store: store} +} + +func (h *Manager) Print(thread string) (string, error) { + var result string + + historyEntries, err := h.store.ReadThread(thread) + if err != nil { + return "", err + } + + var ( + lastRole string + concatenatedMessage string + ) + + for _, entry := range historyEntries { + if entry.Role == userRole && lastRole == userRole { + concatenatedMessage += entry.Content + } else { + if lastRole == userRole && concatenatedMessage != "" { + result += formatHistory(History{ + Message: api.Message{Role: userRole, Content: concatenatedMessage}, + Timestamp: entry.Timestamp, + }) + concatenatedMessage = "" + } + + if entry.Role == userRole { + concatenatedMessage = entry.Content + } else { + result += formatHistory(History{ + Message: entry.Message, + Timestamp: entry.Timestamp, + }) + } + } + + lastRole = entry.Role + } + + // Handle the case where the last entry is a user entry and was concatenated + if lastRole == userRole && concatenatedMessage != "" { + result += formatHistory(History{ + Message: api.Message{Role: userRole, Content: concatenatedMessage}, + }) + } + + return result, nil +} + +func formatHistory(entry History) string { + var ( + emoji string + prefix string + timestamp string + ) + + switch entry.Role { + case systemRole: + emoji = "💻" + prefix = "\n" + case userRole: + emoji = "👤" + prefix = "---\n" + if !entry.Timestamp.IsZero() { + timestamp = fmt.Sprintf(" [%s]", entry.Timestamp.Format("2006-01-02 15:04:05")) + } + case assistantRole: + emoji = "🤖" + prefix = "\n" + } + + return fmt.Sprintf("%s**%s** %s%s:\n%s\n", prefix, strings.ToUpper(entry.Role), emoji, timestamp, entry.Content) +} diff --git a/history/history_test.go b/history/manager_test.go similarity index 76% rename from history/history_test.go rename to history/manager_test.go index b55f318..879c980 100644 --- a/history/history_test.go +++ b/history/manager_test.go @@ -3,8 +3,8 @@ package history_test import ( "errors" "github.com/golang/mock/gomock" + "github.com/kardolus/chatgpt-cli/api" "github.com/kardolus/chatgpt-cli/history" - "github.com/kardolus/chatgpt-cli/types" . "github.com/onsi/gomega" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -16,7 +16,7 @@ import ( var ( mockCtrl *gomock.Controller mockHistoryStore *MockHistoryStore - subject *history.History + subject *history.Manager ) func TestUnitHistory(t *testing.T) { @@ -46,15 +46,15 @@ func testHistory(t *testing.T, when spec.G, it spec.S) { }) it("concatenates multiple user messages", func() { - historyEntries := []types.History{ + historyEntries := []history.History{ { - Message: types.Message{Role: "user", Content: "first message"}, + Message: api.Message{Role: "user", Content: "first message"}, }, { - Message: types.Message{Role: "user", Content: " second message"}, + Message: api.Message{Role: "user", Content: " second message"}, }, { - Message: types.Message{Role: "assistant", Content: "response"}, + Message: api.Message{Role: "assistant", Content: "response"}, }, } @@ -67,15 +67,15 @@ func testHistory(t *testing.T, when spec.G, it spec.S) { }) it("prints all roles correctly", func() { - historyEntries := []types.History{ + historyEntries := []history.History{ { - Message: types.Message{Role: "system", Content: "system message"}, + Message: api.Message{Role: "system", Content: "system message"}, }, { - Message: types.Message{Role: "user", Content: "user message"}, + Message: api.Message{Role: "user", Content: "user message"}, }, { - Message: types.Message{Role: "assistant", Content: "assistant message"}, + Message: api.Message{Role: "assistant", Content: "assistant message"}, }, } @@ -89,12 +89,12 @@ func testHistory(t *testing.T, when spec.G, it spec.S) { }) it("handles the final user message concatenation", func() { - historyEntries := []types.History{ + historyEntries := []history.History{ { - Message: types.Message{Role: "user", Content: "first message"}, + Message: api.Message{Role: "user", Content: "first message"}, }, { - Message: types.Message{Role: "user", Content: " second message"}, + Message: api.Message{Role: "user", Content: " second message"}, }, } diff --git a/history/store.go b/history/store.go index f391824..9e6675d 100644 --- a/history/store.go +++ b/history/store.go @@ -3,8 +3,7 @@ package history import ( "encoding/json" "github.com/kardolus/chatgpt-cli/config" - "github.com/kardolus/chatgpt-cli/types" - "github.com/kardolus/chatgpt-cli/utils" + "github.com/kardolus/chatgpt-cli/internal/utils" "os" "path" "path/filepath" @@ -15,9 +14,9 @@ const ( ) type HistoryStore interface { - Read() ([]types.History, error) - ReadThread(string) ([]types.History, error) - Write([]types.History) error + Read() ([]History, error) + ReadThread(string) ([]History, error) + Write([]History) error SetThread(string) GetThread() string } @@ -68,15 +67,15 @@ func (f *FileIO) WithDirectory(historyDir string) *FileIO { return f } -func (f *FileIO) Read() ([]types.History, error) { +func (f *FileIO) Read() ([]History, error) { return parseFile(f.getPath(f.thread)) } -func (f *FileIO) ReadThread(thread string) ([]types.History, error) { +func (f *FileIO) ReadThread(thread string) ([]History, error) { return parseFile(f.getPath(thread)) } -func (f *FileIO) Write(historyEntries []types.History) error { +func (f *FileIO) Write(historyEntries []History) error { data, err := json.Marshal(historyEntries) if err != nil { return err @@ -107,7 +106,7 @@ func migrate() error { } if !fileInfo.IsDir() { - defaults := config.New().ReadDefaults() + defaults := config.NewStore().ReadDefaults() // move the legacy "history" file to "default.json" if err := os.Rename(historyFile, path.Join(hiddenDir, defaults.Thread+jsonExtension)); err != nil { @@ -128,8 +127,8 @@ func migrate() error { return nil } -func parseFile(fileName string) ([]types.History, error) { - var result []types.History +func parseFile(fileName string) ([]History, error) { + var result []History buf, err := os.ReadFile(fileName) if err != nil { diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..5dedf88 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,47 @@ +package utils + +import ( + "os" + "path/filepath" +) + +const ( + ConfigHomeEnv = "OPENAI_CONFIG_HOME" + DataHomeEnv = "OPENAI_DATA_HOME" + DefaultConfigDir = ".chatgpt-cli" + DefaultDataDir = "history" +) + +func GetConfigHome() (string, error) { + var result string + + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + + result = filepath.Join(homeDir, DefaultConfigDir) + + if tmp := os.Getenv(ConfigHomeEnv); tmp != "" { + result = tmp + } + + return result, nil +} + +func GetDataHome() (string, error) { + var result string + + configHome, err := GetConfigHome() + if err != nil { + return "", err + } + + result = filepath.Join(configHome, DefaultDataDir) + + if tmp := os.Getenv(DataHomeEnv); tmp != "" { + result = tmp + } + + return result, nil +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 0000000..90ad198 --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,60 @@ +package utils_test + +import ( + "github.com/kardolus/chatgpt-cli/internal/utils" + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "os" + "testing" +) + +func TestUnitUtils(t *testing.T) { + spec.Run(t, "Testing the Utils", testUtils, spec.Report(report.Terminal{})) +} + +func testUtils(t *testing.T, when spec.G, it spec.S) { + it.Before(func() { + RegisterTestingT(t) + Expect(os.Unsetenv(utils.ConfigHomeEnv)).To(Succeed()) + Expect(os.Unsetenv(utils.DataHomeEnv)).To(Succeed()) + }) + + when("GetConfigHome()", func() { + it("Uses the default value if OPENAI_CONFIG_HOME is not set", func() { + configHome, err := utils.GetConfigHome() + + Expect(err).NotTo(HaveOccurred()) + Expect(configHome).To(ContainSubstring(".chatgpt-cli")) // Assuming default location is ~/.chatgpt-cli + }) + + it("Overwrites the default when OPENAI_CONFIG_HOME is set", func() { + customConfigHome := "/custom/config/path" + Expect(os.Setenv("OPENAI_CONFIG_HOME", customConfigHome)).To(Succeed()) + + configHome, err := utils.GetConfigHome() + + Expect(err).NotTo(HaveOccurred()) + Expect(configHome).To(Equal(customConfigHome)) + }) + }) + + when("GetDataHome()", func() { + it("Uses the default value if OPENAI_DATA_HOME is not set", func() { + dataHome, err := utils.GetDataHome() + + Expect(err).NotTo(HaveOccurred()) + Expect(dataHome).To(ContainSubstring(".chatgpt-cli/history")) // Assuming default location is ~/.local/share/chatgpt-cli + }) + + it("Overwrites the default when OPENAI_DATA_HOME is set", func() { + customDataHome := "/custom/data/path" + Expect(os.Setenv("OPENAI_DATA_HOME", customDataHome)).To(Succeed()) + + dataHome, err := utils.GetDataHome() + + Expect(err).NotTo(HaveOccurred()) + Expect(dataHome).To(Equal(customDataHome)) + }) + }) +} diff --git a/scripts/run_test.sh b/scripts/run_test.sh index abdd300..850ab11 100755 --- a/scripts/run_test.sh +++ b/scripts/run_test.sh @@ -10,14 +10,9 @@ test_type=$1 cd "$( dirname "${BASH_SOURCE[0]}" )/.." -if [[ ! -d integration ]]; then - echo -e "\n\033[0;31m** WARNING No ${test_type} tests **\033[0m" - exit 0 -fi - echo "Run ${test_type} Tests" set +e -go test -parallel 1 -timeout 0 -mod=vendor ./integration/... -v -run ${test_type} +go test -parallel 1 -timeout 0 -mod=vendor ./test/... -v -run ${test_type} exit_code=$? if [ "$exit_code" != "0" ]; then diff --git a/integration/contract_test.go b/test/contract/contract_test.go similarity index 84% rename from integration/contract_test.go rename to test/contract/contract_test.go index 9a4d633..c9312aa 100644 --- a/integration/contract_test.go +++ b/test/contract/contract_test.go @@ -1,12 +1,11 @@ -package integration_test +package contract_test import ( "encoding/json" - "github.com/kardolus/chatgpt-cli/client" + "github.com/kardolus/chatgpt-cli/api" + "github.com/kardolus/chatgpt-cli/api/client" + "github.com/kardolus/chatgpt-cli/api/http" "github.com/kardolus/chatgpt-cli/config" - "github.com/kardolus/chatgpt-cli/configmanager" - "github.com/kardolus/chatgpt-cli/http" - "github.com/kardolus/chatgpt-cli/types" . "github.com/onsi/gomega" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -21,16 +20,16 @@ func TestContract(t *testing.T) { func testContract(t *testing.T, when spec.G, it spec.S) { var ( restCaller *http.RestCaller - cfg types.Config + cfg config.Config ) it.Before(func() { RegisterTestingT(t) - apiKey := os.Getenv(configmanager.New(config.New()).WithEnvironment().APIKeyEnvVarName()) + apiKey := os.Getenv(config.NewManager(config.NewStore()).WithEnvironment().APIKeyEnvVarName()) Expect(apiKey).NotTo(BeEmpty()) - cfg = config.New().ReadDefaults() + cfg = config.NewStore().ReadDefaults() cfg.APIKey = apiKey restCaller = http.New(cfg) @@ -38,8 +37,8 @@ func testContract(t *testing.T, when spec.G, it spec.S) { when("accessing the completion endpoint", func() { it("should return a successful response with expected keys", func() { - body := types.CompletionsRequest{ - Messages: []types.Message{{ + body := api.CompletionsRequest{ + Messages: []api.Message{{ Role: client.SystemRole, Content: cfg.Role, }}, @@ -54,7 +53,7 @@ func testContract(t *testing.T, when spec.G, it spec.S) { resp, err := restCaller.Post(cfg.URL+cfg.CompletionsPath, bytes, false) Expect(err).NotTo(HaveOccurred()) - var data types.CompletionsResponse + var data api.CompletionsResponse err = json.Unmarshal(resp, &data) Expect(err).NotTo(HaveOccurred()) @@ -67,8 +66,8 @@ func testContract(t *testing.T, when spec.G, it spec.S) { }) it("should return an error response with appropriate error details", func() { - body := types.CompletionsRequest{ - Messages: []types.Message{{ + body := api.CompletionsRequest{ + Messages: []api.Message{{ Role: client.SystemRole, Content: cfg.Role, }}, @@ -82,7 +81,7 @@ func testContract(t *testing.T, when spec.G, it spec.S) { resp, err := restCaller.Post(cfg.URL+cfg.CompletionsPath, bytes, false) Expect(err).To(HaveOccurred()) - var errorData types.ErrorResponse + var errorData api.ErrorResponse err = json.Unmarshal(resp, &errorData) Expect(err).NotTo(HaveOccurred()) @@ -97,7 +96,7 @@ func testContract(t *testing.T, when spec.G, it spec.S) { resp, err := restCaller.Get(cfg.URL + cfg.ModelsPath) Expect(err).NotTo(HaveOccurred()) - var data types.ListModelsResponse + var data api.ListModelsResponse err = json.Unmarshal(resp, &data) Expect(err).NotTo(HaveOccurred()) diff --git a/integration/helpers_test.go b/test/integration/helpers_test.go similarity index 91% rename from integration/helpers_test.go rename to test/integration/helpers_test.go index aa1e843..cc4e3b3 100644 --- a/integration/helpers_test.go +++ b/test/integration/helpers_test.go @@ -4,8 +4,7 @@ import ( "errors" "fmt" "github.com/kardolus/chatgpt-cli/config" - "github.com/kardolus/chatgpt-cli/types" - "github.com/kardolus/chatgpt-cli/utils" + "github.com/kardolus/chatgpt-cli/test" "github.com/onsi/gomega/gexec" "io" "net/http" @@ -50,13 +49,13 @@ func curl(url string) (string, error) { func runMockServer() error { var ( - defaults types.Config + defaults config.Config err error ) onceServe.Do(func() { go func() { - defaults = config.New().ReadDefaults() + defaults = config.NewStore().ReadDefaults() http.HandleFunc("/ping", getPing) http.HandleFunc(defaults.CompletionsPath, postCompletions) @@ -89,7 +88,7 @@ func getModels(w http.ResponseWriter, r *http.Request) { } const modelFile = "models.json" - response, err := utils.FileToBytes(modelFile) + response, err := test.FileToBytes(modelFile) if err != nil { fmt.Printf("error reading %s: %s\n", modelFile, err.Error()) return @@ -109,7 +108,7 @@ func postCompletions(w http.ResponseWriter, r *http.Request) { } const completionsFile = "completions.json" - response, err := utils.FileToBytes(completionsFile) + response, err := test.FileToBytes(completionsFile) if err != nil { fmt.Printf("error reading %s: %s\n", completionsFile, err.Error()) return @@ -139,7 +138,7 @@ func checkBearerToken(r *http.Request, expectedToken string) error { func creatAuthError() string { const errorFile = "error.json" - response, err := utils.FileToBytes(errorFile) + response, err := test.FileToBytes(errorFile) if err != nil { fmt.Printf("error reading %s: %s\n", errorFile, err.Error()) return "" diff --git a/integration/integration_test.go b/test/integration/integration_test.go similarity index 97% rename from integration/integration_test.go rename to test/integration/integration_test.go index d0bada9..1d85c5c 100644 --- a/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -3,11 +3,11 @@ package integration_test import ( "encoding/json" "fmt" + "github.com/kardolus/chatgpt-cli/api" "github.com/kardolus/chatgpt-cli/config" - "github.com/kardolus/chatgpt-cli/configmanager" "github.com/kardolus/chatgpt-cli/history" - "github.com/kardolus/chatgpt-cli/types" - "github.com/kardolus/chatgpt-cli/utils" + "github.com/kardolus/chatgpt-cli/internal/utils" + utils2 "github.com/kardolus/chatgpt-cli/test" "github.com/onsi/gomega/gexec" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -55,7 +55,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { var ( tmpDir string fileIO *history.FileIO - messages []types.Message + messages []api.Message err error ) @@ -67,7 +67,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { fileIO = fileIO.WithDirectory(tmpDir) fileIO.SetThread(threadName) - messages = []types.Message{ + messages = []api.Message{ { Role: "user", Content: "Test message 1", @@ -84,9 +84,9 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { }) it("writes the messages to the file", func() { - var historyEntries []types.History + var historyEntries []history.History for _, message := range messages { - historyEntries = append(historyEntries, types.History{ + historyEntries = append(historyEntries, history.History{ Message: message, }) } @@ -96,9 +96,9 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { }) it("reads the messages from the file", func() { - var historyEntries []types.History + var historyEntries []history.History for _, message := range messages { - historyEntries = append(historyEntries, types.History{ + historyEntries = append(historyEntries, history.History{ Message: message, }) } @@ -118,7 +118,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { tmpFile *os.File historyDir string configIO *config.FileIO - testConfig types.Config + testConfig config.Config err error ) @@ -134,9 +134,9 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { Expect(tmpFile.Close()).To(Succeed()) - configIO = config.New().WithConfigPath(tmpFile.Name()).WithHistoryPath(historyDir) + configIO = config.NewStore().WithConfigPath(tmpFile.Name()).WithHistoryPath(historyDir) - testConfig = types.Config{ + testConfig = config.Config{ Model: "test-model", } }) @@ -146,7 +146,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { }) when("performing a migration", func() { - defaults := config.New().ReadDefaults() + defaults := config.NewStore().ReadDefaults() it("it doesn't apply a migration when max_tokens is 0", func() { testConfig.MaxTokens = 0 @@ -282,7 +282,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { homeDir, err = os.MkdirTemp("", "mockHome") Expect(err).NotTo(HaveOccurred()) - apiKeyEnvVar = configmanager.New(config.New()).WithEnvironment().APIKeyEnvVarName() + apiKeyEnvVar = config.NewManager(config.NewStore()).WithEnvironment().APIKeyEnvVarName() Expect(os.Setenv("HOME", homeDir)).To(Succeed()) Expect(os.Setenv(apiKeyEnvVar, expectedToken)).To(Succeed()) @@ -580,7 +580,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { historyFile := path.Join(filePath, "history", "default.json") Expect(historyFile).NotTo(BeAnExistingFile()) - bytes, err := utils.FileToBytes("history.json") + bytes, err := utils2.FileToBytes("history.json") Expect(err).NotTo(HaveOccurred()) Expect(os.WriteFile(legacyFile, bytes, 0644)).To(Succeed()) @@ -748,7 +748,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { }) it("has a configurable default context-window", func() { - defaults := config.New().ReadDefaults() + defaults := config.NewStore().ReadDefaults() // Initial check for default context-window output := runCommand("--config") @@ -776,7 +776,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { }) it("has a configurable default max-tokens", func() { - defaults := config.New().ReadDefaults() + defaults := config.NewStore().ReadDefaults() // Initial check for default max-tokens output := runCommand("--config") @@ -803,7 +803,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { }) it("has a configurable default thread", func() { - defaults := config.New().ReadDefaults() + defaults := config.NewStore().ReadDefaults() // Initial check for default thread output := runCommand("--config") @@ -898,7 +898,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) historyFile = filepath.Join(tmpDir, "default.json") - messages := []types.Message{ + messages := []api.Message{ {Role: "user", Content: "Hello"}, {Role: "assistant", Content: "Hi, how can I help you?"}, {Role: "user", Content: "Tell me about the weather"}, @@ -931,7 +931,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { specificHistoryFile := filepath.Join(tmpDir, specificThread+".json") // Create a specific thread with custom history - messages := []types.Message{ + messages := []api.Message{ {Role: "user", Content: "What's the capital of Belgium?"}, {Role: "assistant", Content: "The capital of Belgium is Brussels."}, } @@ -949,7 +949,7 @@ func testIntegration(t *testing.T, when spec.G, it spec.S) { it("concatenates user messages correctly", func() { // Create history where two user messages are concatenated - messages := []types.Message{ + messages := []api.Message{ {Role: "user", Content: "Part one"}, {Role: "user", Content: " and part two"}, {Role: "assistant", Content: "This is a response."}, diff --git a/utils/testutils.go b/test/utils.go similarity index 97% rename from utils/testutils.go rename to test/utils.go index 7a3cdd4..004db49 100644 --- a/utils/testutils.go +++ b/test/utils.go @@ -1,4 +1,4 @@ -package utils +package test import ( . "github.com/onsi/gomega" diff --git a/types/history.go b/types/history.go deleted file mode 100644 index 9a39eb5..0000000 --- a/types/history.go +++ /dev/null @@ -1,8 +0,0 @@ -package types - -import "time" - -type History struct { - Message - Timestamp time.Time `json:"timestamp,omitempty"` -}