From cca1ce0247c919878212ab1547bf47efde0b695c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 23:06:46 +0000 Subject: [PATCH 1/2] refactor(writer): Phase 1 cleanup - remove legacy code and consolidate architecture This is Phase 1 of the writer package refactoring, focused on removing legacy code and consolidating the modern strategy pattern architecture. Changes: - Deleted legacy mapper functions from writer.go (CostAndUsageToVectorMapper, CostAndUsageToStdoutMapper, CostAndUsageToCSVMapper, etc.) - Removed old table implementations by deleting stdout.go entirely - Deleted csv.go with legacy CSV helper functions - Removed legacy wrapper types from writers.go (GenericStdoutPrinter, GenericCsvPrinter, GenericChartPrinter, GenericPineconePrinter) - Replaced with a cleaner PrinterAdapter that consolidates all legacy interface compatibility in one place - Cleaned up chart.go by removing WriteToChart function and unused imports - Removed unused global variables (csvFileName, csvHeader) - Added comprehensive package documentation explaining the strategy pattern architecture Impact: - Reduced code by ~200 lines - Eliminated code duplication between old and new implementations - Clearer separation of concerns with dedicated transformers and renderers - All existing functionality preserved through PrinterAdapter - All tests passing, build successful The package now has a clean foundation built on: - Transformer interface: converts domain data to output formats - Renderer interface: writes formatted data to destinations - CompositeWriter: combines transformer + renderer - Factory functions in writers.go for creating specific writer types Next phases will focus on improving the factory pattern and adding dependency injection for better testability. --- internal/writer/chart.go | 22 +---- internal/writer/csv.go | 43 ---------- internal/writer/stdout.go | 169 ------------------------------------ internal/writer/writer.go | 170 +++++++++++++------------------------ internal/writer/writers.go | 83 ------------------ 5 files changed, 58 insertions(+), 429 deletions(-) delete mode 100644 internal/writer/csv.go delete mode 100644 internal/writer/stdout.go diff --git a/internal/writer/chart.go b/internal/writer/chart.go index 7ed7732..396a82d 100644 --- a/internal/writer/chart.go +++ b/internal/writer/chart.go @@ -3,32 +3,12 @@ package writer import ( "fmt" cc "github.com/cduggn/ccexplorer/internal/types" - "github.com/cduggn/ccexplorer/internal/utils" "github.com/go-echarts/go-echarts/v2/charts" "github.com/go-echarts/go-echarts/v2/components" "github.com/go-echarts/go-echarts/v2/opts" - - "io" -) - -var ( - chartFileName = "ccexplorer_chart.html" ) -func WriteToChart(p *components.Page) error { - - f, err := utils.NewFile(OutputDir, chartFileName) - if err != nil { - return cc.Error{ - Msg: "Failed creating chart HTML file: " + err.Error(), - } - } - defer f.Close() - return p.Render(io.MultiWriter(f)) -} - -func (Builder) NewCharts(r cc.InputType) (*components.Page, - error) { +func (Builder) NewCharts(r cc.InputType) (*components.Page, error) { page := components.NewPage() page.PageTitle = "Cost and Usage Report" diff --git a/internal/writer/csv.go b/internal/writer/csv.go deleted file mode 100644 index 0be9c64..0000000 --- a/internal/writer/csv.go +++ /dev/null @@ -1,43 +0,0 @@ -package writer - -import ( - "encoding/csv" - "github.com/cduggn/ccexplorer/internal/types" - "github.com/cduggn/ccexplorer/internal/utils" - "io" - "os" -) - -func WriteToCSV(f *os.File, header []string, rows [][]string) error { - w, err := NewCSVWriter(f, header) - if err != nil { - return types.Error{ - Msg: "Error creating CSV writer: " + err.Error()} - } - defer w.Flush() - - if err := w.WriteAll(rows); err != nil { - return types.Error{ - Msg: "Error writing to CSV file: " + err.Error()} - } - return nil -} - -func NewCSVWriter(f io.Writer, header []string) (*csv.Writer, error) { - w := csv.NewWriter(f) - err := w.Write(header) - if err != nil { - return nil, err - } - return w, nil -} - -func NewCSVFile(dir string, file string) (*os.File, error) { - path := utils.BuildOutputFilePath(dir, file) - f, err := os.Create(path) - if err != nil { - return nil, types.Error{ - Msg: "Error creating CSV file: " + err.Error()} - } - return f, nil -} diff --git a/internal/writer/stdout.go b/internal/writer/stdout.go deleted file mode 100644 index 98f005e..0000000 --- a/internal/writer/stdout.go +++ /dev/null @@ -1,169 +0,0 @@ -package writer - -import ( - "fmt" - "github.com/cduggn/ccexplorer/internal/types" - "github.com/cduggn/ccexplorer/internal/utils" - "github.com/jedib0t/go-pretty/v6/table" - "os" -) - -var ( - tableDivider = table.Row{"", "", "", - "", "", "", "", - "", - "", ""} - costAndUsageHeader = table.Row{"Rank", "Dimension/Tag", "Dimension/Tag", - "Metric Name", "Amount", "Rounded", - "Unit", - "Granularity", - "Start", - "End"} - costAndUsageTableFooter = func(t string) table.Row { - return table. - Row{"", "", - "", - "", - "Cost", - t, "", "", "", ""} - } - forecastedHeader = table.Row{"Start", "End", "Mean Value", - "Prediction Interval LowerBound", - "Prediction Interval UpperBound", "Unit", "Total"} - forecastedTableFooter = func(filter string, unit string, - amount string) table.Row { - return table.Row{"FilteredBy", filter, "", "", "", - unit, - amount} - } - tableStyleColors = table.StyleColoredGreenWhiteOnBlack -) - -type CostAndUsageTable struct { - Table table.Writer -} - -type ForecastTable struct { - Table table.Writer -} - -func NewStdoutWriter(variant string) (types.Table, error) { - switch variant { - case "forecast": - return ForecastTable{ - Table: table.NewWriter(), - }, nil - case "costAndUsage": - return CostAndUsageTable{ - Table: table.NewWriter(), - }, nil - } - - return nil, fmt.Errorf("unknown table type: %s", variant) -} - -func (c CostAndUsageTable) Writer(output interface{}) { - outputType := output.(types.CostAndUsageStdoutType) - c.Style() - c.Header() - rows := CostUsageToRows(outputType.Services, outputType.Granularity) - - c.AddRows(rows.Rows) - c.Table.AppendRow(tableDivider) - c.Footer(costAndUsageTableFooter(rows.Total)) - c.Table.Render() -} - -func (c CostAndUsageTable) Style() { - c.Table.SetOutputMirror(os.Stdout) - c.Table.SetColumnConfigs( - []table.ColumnConfig{ - {Number: 6, WidthMax: 8}, - }) - c.Table.SetStyle(tableStyleColors) - - c.Table.SuppressEmptyColumns() -} - -func (c CostAndUsageTable) Header() { - c.Table.AppendHeader(costAndUsageHeader) -} - -func (c CostAndUsageTable) AddRows(rows []table.Row) { - c.Table.AppendRows(rows) -} - -func (c CostAndUsageTable) Footer(row table.Row) { - c.Table.AppendFooter(row) -} - -func (f ForecastTable) Writer(output interface{}) { - outputType := output.(types.ForecastStdoutType) - //f.Table.SuppressEmptyColumns() - f.Style() - f.Header() - rows := ForecastToRows(outputType) - f.AddRows(rows) - - f.Footer(forecastedTableFooter(outputType.FilteredBy, - outputType.Total.Unit, outputType.Total.Amount)) - - f.Table.Render() -} - -func (f ForecastTable) Style() { - f.Table.SetOutputMirror(os.Stdout) - f.Table.SetStyle(tableStyleColors) -} - -func (f ForecastTable) Footer(row table.Row) { - f.Table.AppendFooter(row) -} - -func (f ForecastTable) AddRows(rows []table.Row) { - f.Table.AppendRows(rows) -} - -func (f ForecastTable) Header() { - f.Table.AppendHeader(forecastedHeader) -} - -func CostUsageToRows(s []types.Service, granularity string) types.CostAndUsage { - var rows []table.Row - var total float64 - for index, v := range s { - for _, m := range v.Metrics { - - if index%10 == 0 { - rows = append(rows, tableDivider) - } - if m.Unit == "USD" { - total += m.NumericAmount - } - - tempRow := table.Row{index + 1, v.Keys[0], - utils.ReturnIfPresent(v.Keys), - m.Name, m.Amount, fmt.Sprintf("%.2f", - m.NumericAmount), - m.Unit, - granularity, - v.Start, v.End} - - rows = append(rows, tempRow) - } - } - totalFormatted := fmt.Sprintf("$%.2f", total) - return types.CostAndUsage{Rows: rows, Total: totalFormatted} -} - -func ForecastToRows(r types.ForecastStdoutType) []table.Row { - var rows []table.Row - for _, v := range r.Forecast { - tempRow := table.Row{v.TimePeriod.Start, - v.TimePeriod.End, v.MeanValue, v.PredictionIntervalUpperBound, - v.PredictionIntervalLowerBound} - - rows = append(rows, tempRow) - } - return rows -} diff --git a/internal/writer/writer.go b/internal/writer/writer.go index d7ccfab..c77b5b3 100644 --- a/internal/writer/writer.go +++ b/internal/writer/writer.go @@ -1,31 +1,27 @@ +// Package writer provides output formatting for AWS cost data. +// +// The package uses a strategy pattern with composable transformers and renderers. +// The main components are: +// - Transformers: convert domain data to output-specific formats +// - Renderers: write formatted data to destinations (stdout, files, vector DB) +// - CompositeWriter: combines a transformer and renderer +// +// For new code, use the factory functions in writers.go directly. +// The PrinterAdapter type provides backward compatibility with legacy code. package writer import ( - "context" - "fmt" - "github.com/cduggn/ccexplorer/internal/http" "github.com/cduggn/ccexplorer/internal/types" - "github.com/cduggn/ccexplorer/internal/utils" - "log/slog" "os" - "strings" ) var ( - csvFileName = "ccexplorer.csv" - csvHeader = []string{"Dimension/Tag", "Dimension/Tag", "Metric", - "Granularity", - "Start", - "End", "USD Amount", "Unit"} OutputDir = "./writer" ) type Builder struct { } -// Legacy printer types - kept for interface compatibility -// Actual implementations are now in writers.go using generics - func init() { if _, err := os.Stat(OutputDir); os.IsNotExist(err) { err := os.Mkdir(OutputDir, 0755) @@ -35,123 +31,71 @@ func init() { } } +// PrinterAdapter adapts the new CompositeWriter to the legacy Printer interface +type PrinterAdapter struct { + printType types.PrintWriterType + variant string +} + func NewPrintWriter(printType types.PrintWriterType, variant string) Printer { - switch printType { + return &PrinterAdapter{ + printType: printType, + variant: variant, + } +} + +// Write implements the Printer interface by delegating to appropriate CompositeWriter +func (p *PrinterAdapter) Write(f interface{}, c interface{}) error { + switch p.printType { case types.Stdout: - return NewGenericStdoutPrinter(variant) + return p.writeStdout(f, c) case types.CSV: - return NewGenericCsvPrinter(variant) + return p.writeCSV(f, c) case types.Chart: - return NewGenericChartPrinter(variant) + return p.writeChart(f, c) case types.Pinecone: - return NewGenericPineconePrinter(variant) + return p.writeVector(f, c) default: panic("Invalid print type") } } -// Legacy mapper functions - kept for backward compatibility but will be removed -// These are now handled by the generic transformers and renderers - -func CostAndUsageToVectorMapper(r types.CostAndUsageOutputType) error { - - client := NewVectorStoreClient(http.NewRequestBuilder(), - r.OpenAIAPIKey, r.PineconeIndex, r.PineconeAPIKey) - items, err := client.CreateVectorStoreInput(r) - if err != nil { - return types.Error{ - Msg: "Error writing to vector store: " + err.Error()} - } - - vectors, err := client.CreateEmbeddings(items) - if err != nil { - return types.Error{ - Msg: "Error writing to vector store: " + err.Error()} - } - - for index, m := range vectors { - items[index].EmbeddingVector = m.Embedding - items[index].ID = utils.EncodeString(items[index].EmbeddingText) - } - - input := utils.ConvertToPineconeStruct(items) - - resp, err := client.Upsert(context.Background(), input) - if err != nil { - return types.Error{ - Msg: "Error writing to vector store: " + err.Error()} - } - - slog.Info(fmt.Sprintf("Upserted %d items to vector store", resp.UpsertedCount)) - return nil -} - -func CostAndUsageToStdoutMapper(sortFn func(r map[int]types.Service) []types.Service, - r types.CostAndUsageOutputType) error { - - sortedServices := sortFn(r.Services) - output := utils.ConvertToStdoutType(sortedServices, r.Granularity) - - w, err := NewStdoutWriter("costAndUsage") - if err != nil { - return types.Error{ - Msg: "Error writing to stdout : " + err.Error()} +func (p *PrinterAdapter) writeStdout(f interface{}, c interface{}) error { + switch p.variant { + case "forecast": + writer := NewForecastTableWriter() + return writer.Write(f.(types.ForecastPrintData)) + case "costAndUsage": + sortBy := f.(string) + writer := NewCostUsageTableWriter(sortBy) + return writer.Write(c.(types.CostAndUsageOutputType)) + default: + panic("Invalid stdout variant: " + p.variant) } - w.Writer(output) - return nil } -func CostAndUsageToCSVMapper(sortFn func(r map[int]types.Service) []types.Service, - r types.CostAndUsageOutputType) error { - - f, err := NewCSVFile(OutputDir, csvFileName) - if err != nil { - return types.Error{ - Msg: "Error creating CSV file: " + err.Error()} - } - defer func(f *os.File) { - err := f.Close() - if err != nil { - panic(err) - } - }(f) - - rows := utils.ConvertServiceMapToArray(r.Services, r.Granularity) - err = WriteToCSV(f, csvHeader, rows) - if err != nil { - return types.Error{ - Msg: "Error writing to CSV file: " + err.Error()} +func (p *PrinterAdapter) writeCSV(f interface{}, c interface{}) error { + if p.variant != "costAndUsage" { + panic("Invalid CSV variant: " + p.variant) } - return nil + sortBy := f.(string) + writer := NewCostUsageCSVWriter(sortBy) + return writer.Write(c.(types.CostAndUsageOutputType)) } -func CostAndUsageToChartMapper(sortFn func(r map[int]types.Service) []types.Service, - r types.CostAndUsageOutputType) error { - - builder := Builder{} - s := sortFn(r.Services) - input := utils.ConvertToChartInputType(r, s) - - charts, err := builder.NewCharts(input) - if err != nil { - return err - } - - err = WriteToChart(charts) - if err != nil { - return err +func (p *PrinterAdapter) writeChart(f interface{}, c interface{}) error { + if p.variant != "costAndUsage" { + panic("Invalid chart variant: " + p.variant) } - return nil + sortBy := f.(string) + writer := NewCostUsageChartWriter(sortBy) + return writer.Write(c.(types.CostAndUsageOutputType)) } -func ForecastToStdoutMapper(r types.ForecastPrintData, - dimensions []string) { - - filteredBy := strings.Join(dimensions, " | ") - output := utils.ConvertToForecastStdoutType(r, filteredBy) - w, err := NewStdoutWriter("forecast") - if err != nil { - return +func (p *PrinterAdapter) writeVector(f interface{}, c interface{}) error { + if p.variant != "costAndUsage" { + panic("Invalid vector variant: " + p.variant) } - w.Writer(output) + writer := NewCostUsageVectorWriter() + return writer.Write(c.(types.CostAndUsageOutputType)) } diff --git a/internal/writer/writers.go b/internal/writer/writers.go index 2f453d6..79fc88b 100644 --- a/internal/writer/writers.go +++ b/internal/writer/writers.go @@ -46,87 +46,4 @@ func NewForecastTableWriter() *ForecastTableWriter { transformer := NewForecastToTableTransformer() renderer := NewForecastTableRenderer() return NewCompositeWriter[types.ForecastPrintData, *ForecastTableOutput](transformer, renderer) -} - -// Legacy compatibility types - these wrap the new generic writers to maintain the old interface -type GenericStdoutPrinter struct { - variant string -} - -type GenericCsvPrinter struct { - variant string -} - -type GenericChartPrinter struct { - variant string -} - -type GenericPineconePrinter struct { - variant string -} - -// NewGenericStdoutPrinter creates a new stdout printer with backward compatibility -func NewGenericStdoutPrinter(variant string) *GenericStdoutPrinter { - return &GenericStdoutPrinter{variant: variant} -} - -// Write implements the legacy Printer interface for stdout -func (p *GenericStdoutPrinter) Write(f interface{}, c interface{}) error { - switch p.variant { - case "forecast": - writer := NewForecastTableWriter() - return writer.Write(f.(types.ForecastPrintData)) - case "costAndUsage": - sortBy := f.(string) - writer := NewCostUsageTableWriter(sortBy) - return writer.Write(c.(types.CostAndUsageOutputType)) - } - return nil -} - -// NewGenericCsvPrinter creates a new CSV printer with backward compatibility -func NewGenericCsvPrinter(variant string) *GenericCsvPrinter { - return &GenericCsvPrinter{variant: variant} -} - -// Write implements the legacy Printer interface for CSV -func (p *GenericCsvPrinter) Write(f interface{}, c interface{}) error { - switch p.variant { - case "costAndUsage": - sortBy := f.(string) - writer := NewCostUsageCSVWriter(sortBy) - return writer.Write(c.(types.CostAndUsageOutputType)) - } - return nil -} - -// NewGenericChartPrinter creates a new chart printer with backward compatibility -func NewGenericChartPrinter(variant string) *GenericChartPrinter { - return &GenericChartPrinter{variant: variant} -} - -// Write implements the legacy Printer interface for charts -func (p *GenericChartPrinter) Write(f interface{}, c interface{}) error { - switch p.variant { - case "costAndUsage": - sortBy := f.(string) - writer := NewCostUsageChartWriter(sortBy) - return writer.Write(c.(types.CostAndUsageOutputType)) - } - return nil -} - -// NewGenericPineconePrinter creates a new Pinecone printer with backward compatibility -func NewGenericPineconePrinter(variant string) *GenericPineconePrinter { - return &GenericPineconePrinter{variant: variant} -} - -// Write implements the legacy Printer interface for vector databases -func (p *GenericPineconePrinter) Write(f interface{}, c interface{}) error { - switch p.variant { - case "costAndUsage": - writer := NewCostUsageVectorWriter() - return writer.Write(c.(types.CostAndUsageOutputType)) - } - return nil } \ No newline at end of file From cb676d3b044469ecb5c146a34dbf529d8ad5db81 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 23:26:26 +0000 Subject: [PATCH 2/2] feat(writer): Phase 2 & 3 - Add dependency injection, functional options, and type-safe factory This commit implements Phase 2 and Phase 3 of the writer refactoring: - Dependency injection for configuration - Functional options pattern - Type-safe factory with improved API Changes: - Created Config struct for centralizing writer configuration (config.go) - Implemented functional options pattern (WithOutputDir, WithSortBy, WithVectorDBConfig, etc.) - Added WriterFactory for type-safe writer creation (factory.go) - Updated all renderers to accept Config via dependency injection - Modified factory functions to use variadic options instead of individual parameters - Removed hardcoded values (OutputDir, API keys) from renderers - Updated PrinterAdapter to use functional options for backward compatibility Architecture improvements: - Renderers no longer depend on global state - Configuration is explicitly passed through the call chain - Better testability via injected dependencies - Type-safe factory methods eliminate runtime type assertions for new code - Flexible configuration through functional options Impact: - All tests passing - Build successful - Backward compatible through PrinterAdapter - New code can use type-safe factory functions - Better separation of concerns and testability Example usage of new API: ```go // Using functional options writer := writer.NewCostUsageCSVWriter( writer.WithSortBy("date"), writer.WithOutputDir("/tmp"), ) writer.Write(data) // Using factory factory := writer.NewWriterFactory( writer.WithSortBy("amount"), writer.WithVectorDBConfig(openAIKey, pineconeKey, index), ) factory.WriteCostUsage(writer.FormatCSV, data) ``` --- internal/writer/config.go | 73 ++++++++++++++++++++++++++++++++++ internal/writer/factory.go | 76 ++++++++++++++++++++++++++++++++++++ internal/writer/renderers.go | 51 +++++++++++++----------- internal/writer/writer.go | 13 +++--- internal/writer/writers.go | 27 +++++++------ 5 files changed, 202 insertions(+), 38 deletions(-) create mode 100644 internal/writer/config.go create mode 100644 internal/writer/factory.go diff --git a/internal/writer/config.go b/internal/writer/config.go new file mode 100644 index 0000000..70ecd81 --- /dev/null +++ b/internal/writer/config.go @@ -0,0 +1,73 @@ +package writer + +import ( + "github.com/cduggn/ccexplorer/internal/http" +) + +// Config holds configuration for writer components +type Config struct { + // OutputDir is the directory where files are written + OutputDir string + + // Vector database configuration + OpenAIAPIKey string + PineconeAPIKey string + PineconeIndex string + + // HTTP client for external services + HTTPClient *http.HttpRequestBuilder + + // Sorting configuration + SortBy string +} + +// Option is a functional option for configuring writers +type Option func(*Config) + +// WithOutputDir sets the output directory for file-based writers +func WithOutputDir(dir string) Option { + return func(c *Config) { + c.OutputDir = dir + } +} + +// WithVectorDBConfig sets the vector database configuration +func WithVectorDBConfig(openAIKey, pineconeKey, pineconeIndex string) Option { + return func(c *Config) { + c.OpenAIAPIKey = openAIKey + c.PineconeAPIKey = pineconeKey + c.PineconeIndex = pineconeIndex + } +} + +// WithHTTPClient sets the HTTP client for external services +func WithHTTPClient(client *http.HttpRequestBuilder) Option { + return func(c *Config) { + c.HTTPClient = client + } +} + +// WithSortBy sets the sorting preference +func WithSortBy(sortBy string) Option { + return func(c *Config) { + c.SortBy = sortBy + } +} + +// DefaultConfig returns a Config with sensible defaults +func DefaultConfig() *Config { + return &Config{ + OutputDir: "./writer", + SortBy: "amount", + HTTPClient: http.NewRequestBuilder(), + } +} + +// NewConfig creates a Config with the given options +func NewConfig(opts ...Option) *Config { + config := DefaultConfig() + for _, opt := range opts { + opt(config) + } + return config +} diff --git a/internal/writer/factory.go b/internal/writer/factory.go new file mode 100644 index 0000000..af6b9f1 --- /dev/null +++ b/internal/writer/factory.go @@ -0,0 +1,76 @@ +package writer + +import ( + "fmt" + + "github.com/cduggn/ccexplorer/internal/types" +) + +// WriterFactory provides type-safe writer creation +type WriterFactory struct { + config *Config +} + +// NewWriterFactory creates a new factory with the given configuration +func NewWriterFactory(opts ...Option) *WriterFactory { + return &WriterFactory{ + config: NewConfig(opts...), + } +} + +// CreateCostUsageWriter creates a writer for cost and usage data +func (f *WriterFactory) CreateCostUsageWriter(format FormatType) (interface{}, error) { + switch format { + case FormatTable: + return NewCostUsageTableWriter(f.configToOptions()...), nil + case FormatCSV: + return NewCostUsageCSVWriter(f.configToOptions()...), nil + case FormatChart: + return NewCostUsageChartWriter(f.configToOptions()...), nil + case FormatVector: + return NewCostUsageVectorWriter(f.configToOptions()...), nil + default: + return nil, fmt.Errorf("unsupported format: %v", format) + } +} + +// CreateForecastWriter creates a writer for forecast data +func (f *WriterFactory) CreateForecastWriter() *ForecastTableWriter { + return NewForecastTableWriter(f.configToOptions()...) +} + +// WriteCostUsage is a type-safe convenience method for writing cost usage data +func (f *WriterFactory) WriteCostUsage(format FormatType, data types.CostAndUsageOutputType) error { + switch format { + case FormatTable: + writer := NewCostUsageTableWriter(f.configToOptions()...) + return writer.Write(data) + case FormatCSV: + writer := NewCostUsageCSVWriter(f.configToOptions()...) + return writer.Write(data) + case FormatChart: + writer := NewCostUsageChartWriter(f.configToOptions()...) + return writer.Write(data) + case FormatVector: + writer := NewCostUsageVectorWriter(f.configToOptions()...) + return writer.Write(data) + default: + return fmt.Errorf("unsupported format: %v", format) + } +} + +// WriteForecast is a type-safe convenience method for writing forecast data +func (f *WriterFactory) WriteForecast(data types.ForecastPrintData) error { + writer := NewForecastTableWriter(f.configToOptions()...) + return writer.Write(data) +} + +// configToOptions converts the factory's config to a slice of options +func (f *WriterFactory) configToOptions() []Option { + return []Option{ + WithOutputDir(f.config.OutputDir), + WithSortBy(f.config.SortBy), + WithVectorDBConfig(f.config.OpenAIAPIKey, f.config.PineconeAPIKey, f.config.PineconeIndex), + WithHTTPClient(f.config.HTTPClient), + } +} diff --git a/internal/writer/renderers.go b/internal/writer/renderers.go index d76c6bf..cdbc8ec 100644 --- a/internal/writer/renderers.go +++ b/internal/writer/renderers.go @@ -7,7 +7,6 @@ import ( "io" "os" - "github.com/cduggn/ccexplorer/internal/http" "github.com/cduggn/ccexplorer/internal/types" "github.com/cduggn/ccexplorer/internal/utils" "github.com/jedib0t/go-pretty/v6/table" @@ -112,16 +111,18 @@ func (r *ForecastTableRenderer) Render(data *ForecastTableOutput) error { } // CSVRenderer renders CSV data to files -type CSVRenderer struct{} +type CSVRenderer struct { + config *Config +} // NewCSVRenderer creates a new CSV renderer -func NewCSVRenderer() *CSVRenderer { - return &CSVRenderer{} +func NewCSVRenderer(config *Config) *CSVRenderer { + return &CSVRenderer{config: config} } // Render implements the Renderer interface for CSV files func (r *CSVRenderer) Render(data *CSVOutput) error { - filePath := utils.BuildOutputFilePath(OutputDir, data.Filename) + filePath := utils.BuildOutputFilePath(r.config.OutputDir, data.Filename) file, err := os.Create(filePath) if err != nil { return types.Error{Msg: "Error creating CSV file: " + err.Error()} @@ -145,16 +146,18 @@ func (r *CSVRenderer) Render(data *CSVOutput) error { } // ChartRenderer renders chart data to HTML files -type ChartRenderer struct{} +type ChartRenderer struct { + config *Config +} // NewChartRenderer creates a new chart renderer -func NewChartRenderer() *ChartRenderer { - return &ChartRenderer{} +func NewChartRenderer(config *Config) *ChartRenderer { + return &ChartRenderer{config: config} } // Render implements the Renderer interface for charts func (r *ChartRenderer) Render(data *ChartOutput) error { - filePath := utils.BuildOutputFilePath(OutputDir, data.Filename) + filePath := utils.BuildOutputFilePath(r.config.OutputDir, data.Filename) file, err := os.Create(filePath) if err != nil { return types.Error{Msg: "Failed creating chart HTML file: " + err.Error()} @@ -165,25 +168,29 @@ func (r *ChartRenderer) Render(data *ChartOutput) error { } // VectorRenderer renders vector data to vector databases -type VectorRenderer struct{} +type VectorRenderer struct { + config *Config + client VectorStore +} // NewVectorRenderer creates a new vector renderer -func NewVectorRenderer() *VectorRenderer { - return &VectorRenderer{} +func NewVectorRenderer(config *Config) *VectorRenderer { + client := NewVectorStoreClient( + config.HTTPClient, + config.OpenAIAPIKey, + config.PineconeIndex, + config.PineconeAPIKey, + ) + return &VectorRenderer{ + config: config, + client: client, + } } // Render implements the Renderer interface for vector databases func (r *VectorRenderer) Render(data *VectorOutput) error { - // Create the vector store client - this would need to be passed in or configured - client := NewVectorStoreClient( - http.NewRequestBuilder(), - data.IndexName, - "", // API keys would need to be provided - "", - ) - // Create embeddings for the vector items - vectors, err := client.CreateEmbeddings(data.Items) + vectors, err := r.client.CreateEmbeddings(data.Items) if err != nil { return types.Error{Msg: "Error creating embeddings: " + err.Error()} } @@ -198,7 +205,7 @@ func (r *VectorRenderer) Render(data *VectorOutput) error { // Convert to Pinecone format and upsert pineconeData := utils.ConvertToPineconeStruct(data.Items) - resp, err := client.Upsert(context.Background(), pineconeData) + resp, err := r.client.Upsert(context.Background(), pineconeData) if err != nil { return types.Error{Msg: "Error upserting to vector store: " + err.Error()} } diff --git a/internal/writer/writer.go b/internal/writer/writer.go index c77b5b3..2b58e89 100644 --- a/internal/writer/writer.go +++ b/internal/writer/writer.go @@ -67,7 +67,7 @@ func (p *PrinterAdapter) writeStdout(f interface{}, c interface{}) error { return writer.Write(f.(types.ForecastPrintData)) case "costAndUsage": sortBy := f.(string) - writer := NewCostUsageTableWriter(sortBy) + writer := NewCostUsageTableWriter(WithSortBy(sortBy)) return writer.Write(c.(types.CostAndUsageOutputType)) default: panic("Invalid stdout variant: " + p.variant) @@ -79,7 +79,7 @@ func (p *PrinterAdapter) writeCSV(f interface{}, c interface{}) error { panic("Invalid CSV variant: " + p.variant) } sortBy := f.(string) - writer := NewCostUsageCSVWriter(sortBy) + writer := NewCostUsageCSVWriter(WithSortBy(sortBy)) return writer.Write(c.(types.CostAndUsageOutputType)) } @@ -88,7 +88,7 @@ func (p *PrinterAdapter) writeChart(f interface{}, c interface{}) error { panic("Invalid chart variant: " + p.variant) } sortBy := f.(string) - writer := NewCostUsageChartWriter(sortBy) + writer := NewCostUsageChartWriter(WithSortBy(sortBy)) return writer.Write(c.(types.CostAndUsageOutputType)) } @@ -96,6 +96,9 @@ func (p *PrinterAdapter) writeVector(f interface{}, c interface{}) error { if p.variant != "costAndUsage" { panic("Invalid vector variant: " + p.variant) } - writer := NewCostUsageVectorWriter() - return writer.Write(c.(types.CostAndUsageOutputType)) + data := c.(types.CostAndUsageOutputType) + writer := NewCostUsageVectorWriter( + WithVectorDBConfig(data.OpenAIAPIKey, data.PineconeAPIKey, data.PineconeIndex), + ) + return writer.Write(data) } diff --git a/internal/writer/writers.go b/internal/writer/writers.go index 79fc88b..1c176d5 100644 --- a/internal/writer/writers.go +++ b/internal/writer/writers.go @@ -12,37 +12,42 @@ type CostUsageVectorWriter = CompositeWriter[types.CostAndUsageOutputType, *Vect type ForecastTableWriter = CompositeWriter[types.ForecastPrintData, *ForecastTableOutput] // Factory functions for creating specific writer types +// All factory functions accept options for configuration // NewCostUsageTableWriter creates a writer for cost usage table output -func NewCostUsageTableWriter(sortBy string) *CostUsageTableWriter { - transformer := NewCostUsageToTableTransformer(sortBy) +func NewCostUsageTableWriter(opts ...Option) *CostUsageTableWriter { + config := NewConfig(opts...) + transformer := NewCostUsageToTableTransformer(config.SortBy) renderer := NewStdoutTableRenderer("costAndUsage") return NewCompositeWriter[types.CostAndUsageOutputType, *TableOutput](transformer, renderer) } // NewCostUsageCSVWriter creates a writer for cost usage CSV output -func NewCostUsageCSVWriter(sortBy string) *CostUsageCSVWriter { - transformer := NewCostUsageToCSVTransformer(sortBy) - renderer := NewCSVRenderer() +func NewCostUsageCSVWriter(opts ...Option) *CostUsageCSVWriter { + config := NewConfig(opts...) + transformer := NewCostUsageToCSVTransformer(config.SortBy) + renderer := NewCSVRenderer(config) return NewCompositeWriter[types.CostAndUsageOutputType, *CSVOutput](transformer, renderer) } // NewCostUsageChartWriter creates a writer for cost usage chart output -func NewCostUsageChartWriter(sortBy string) *CostUsageChartWriter { - transformer := NewCostUsageToChartTransformer(sortBy) - renderer := NewChartRenderer() +func NewCostUsageChartWriter(opts ...Option) *CostUsageChartWriter { + config := NewConfig(opts...) + transformer := NewCostUsageToChartTransformer(config.SortBy) + renderer := NewChartRenderer(config) return NewCompositeWriter[types.CostAndUsageOutputType, *ChartOutput](transformer, renderer) } // NewCostUsageVectorWriter creates a writer for cost usage vector output -func NewCostUsageVectorWriter() *CostUsageVectorWriter { +func NewCostUsageVectorWriter(opts ...Option) *CostUsageVectorWriter { + config := NewConfig(opts...) transformer := NewCostUsageToVectorTransformer() - renderer := NewVectorRenderer() + renderer := NewVectorRenderer(config) return NewCompositeWriter[types.CostAndUsageOutputType, *VectorOutput](transformer, renderer) } // NewForecastTableWriter creates a writer for forecast table output -func NewForecastTableWriter() *ForecastTableWriter { +func NewForecastTableWriter(opts ...Option) *ForecastTableWriter { transformer := NewForecastToTableTransformer() renderer := NewForecastTableRenderer() return NewCompositeWriter[types.ForecastPrintData, *ForecastTableOutput](transformer, renderer)