Skip to content

Commit

Permalink
Making concatenate package publicly usable (#2)
Browse files Browse the repository at this point in the history
* Making concatenator as public package

* Re-named the constructor name

---------

Co-authored-by: Shaharia Azam <hello@shaharialab.com>
  • Loading branch information
shahariaazam and hello-shaharia-lab authored Sep 4, 2024
1 parent 4671fc9 commit 88c4241
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 130 deletions.
134 changes: 13 additions & 121 deletions cora.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
package main

import (
"bufio"
"fmt"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"strings"

"github.com/shaharia-lab/cora/pkg/concatenator"
"github.com/spf13/cobra"
)

const (
defaultBufferSize = 64 * 1024 // 64KB
)

func main() {
if err := newRootCmd().Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
Expand All @@ -34,7 +28,7 @@ func newRootCmd() *cobra.Command {
RunE: cfg.run,
}

rootCmd.Flags().StringVarP(&cfg.SourceDirectory, "source", "s", "", "Source directory to concatenate files from")
rootCmd.Flags().StringVarP(&cfg.SourceDirectory, "source", "s", "", "Source directory to Concatenate files from")
rootCmd.Flags().StringVarP(&cfg.OutputFile, "output", "o", "", "Output file to write concatenated files to")
rootCmd.Flags().StringSliceVarP(&cfg.ExcludePatterns, "exclude", "e", nil, "Glob patterns to exclude")
rootCmd.Flags().StringSliceVarP(&cfg.IncludePatterns, "include", "i", nil, "Glob patterns to include")
Expand All @@ -60,9 +54,9 @@ func (cfg *config) run(cmd *cobra.Command, args []string) error {
return err
}

debugLog := newDebugLog(cfg.EnableDebugging)
debugLog := concatenator.NewDebugLog(cfg.EnableDebugging)
w := newWalker(cfg.SourceDirectory, cfg.ExcludePatterns, cfg.IncludePatterns, debugLog)
c := newConcatenator(cfg.OutputFile, cfg.Separator, cfg.PathPrefix, debugLog)
c := concatenator.NewConcatenation(cfg.OutputFile, cfg.Separator, cfg.PathPrefix, debugLog)

return cfg.process(w, c)
}
Expand All @@ -77,14 +71,14 @@ func (cfg *config) validate() error {
return nil
}

func (cfg *config) process(w *walker, c *concatenator) error {
func (cfg *config) process(w *walker, c *concatenator.Concatenator) error {
filePaths, err := w.walk()
if err != nil {
return fmt.Errorf("failed to walk directory: %w", err)
}

if err := c.concatenate(filePaths); err != nil {
return fmt.Errorf("failed to concatenate files: %w", err)
if err := c.Concatenate(filePaths); err != nil {
return fmt.Errorf("failed to Concatenate files: %w", err)
}

return nil
Expand All @@ -94,10 +88,10 @@ type walker struct {
sourceDirectory string
excludePatterns []string
includePatterns []string
debugLog *debugLog
debugLog *concatenator.DebugLog
}

func newWalker(sourceDirectory string, excludePatterns, includePatterns []string, debugLog *debugLog) *walker {
func newWalker(sourceDirectory string, excludePatterns, includePatterns []string, debugLog *concatenator.DebugLog) *walker {
return &walker{
sourceDirectory: sourceDirectory,
excludePatterns: excludePatterns,
Expand All @@ -120,7 +114,7 @@ func (w *walker) walk() ([]string, error) {
}

if excluded {
w.debugLog.print(fmt.Sprintf("Excluding %s", path))
w.debugLog.Print(fmt.Sprintf("Excluding %s", path))
if d.IsDir() {
return filepath.SkipDir
}
Expand All @@ -135,13 +129,13 @@ func (w *walker) walk() ([]string, error) {
}
if included {
files = append(files, path)
w.debugLog.print(fmt.Sprintf("Including %s", path))
w.debugLog.Print(fmt.Sprintf("Including %s", path))
} else {
w.debugLog.print(fmt.Sprintf("Skipping %s (not in include patterns)", path))
w.debugLog.Print(fmt.Sprintf("Skipping %s (not in include patterns)", path))
}
} else {
files = append(files, path)
w.debugLog.print(fmt.Sprintf("Including %s", path))
w.debugLog.Print(fmt.Sprintf("Including %s", path))
}
}

Expand All @@ -151,90 +145,6 @@ func (w *walker) walk() ([]string, error) {
return files, err
}

type concatenator struct {
outputPath string
separator []byte
debugLog *debugLog
pathPrefix []byte
}

func newConcatenator(outputPath, separator, pathPrefix string, debugLog *debugLog) *concatenator {
return &concatenator{
outputPath: outputPath,
separator: []byte(separator),
pathPrefix: []byte(pathPrefix),
debugLog: debugLog,
}
}

func (c *concatenator) concatenate(filePaths []string) error {
if err := os.MkdirAll(filepath.Dir(c.outputPath), 0755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}

outFile, err := os.Create(c.outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer outFile.Close()

writer := bufio.NewWriterSize(outFile, defaultBufferSize)
defer writer.Flush()

for i, filePath := range filePaths {
if i > 0 {
if _, err := writer.Write(c.separator); err != nil {
return fmt.Errorf("failed to write separator: %w", err)
}
}

if err := c.writeFileHeader(writer, filePath); err != nil {
return err
}

if err := c.appendFileContent(writer, filePath); err != nil {
return err
}

if _, err := writer.Write([]byte{'\n'}); err != nil {
return fmt.Errorf("failed to write newline after file content: %w", err)
}
}

return nil
}

func (c *concatenator) writeFileHeader(writer *bufio.Writer, filePath string) error {
if _, err := writer.Write(c.pathPrefix); err != nil {
return fmt.Errorf("failed to write path prefix: %w", err)
}

if _, err := writer.WriteString(filePath); err != nil {
return fmt.Errorf("failed to write file path: %w", err)
}

if _, err := writer.Write([]byte{'\n'}); err != nil {
return fmt.Errorf("failed to write newline after file path: %w", err)
}

return nil
}

func (c *concatenator) appendFileContent(writer *bufio.Writer, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", filePath, err)
}
defer file.Close()

_, err = io.Copy(writer, file)
if err != nil {
return fmt.Errorf("failed to copy content from %s: %w", filePath, err)
}

return nil
}

func matchesGlob(rootPath, filePath string, patterns []string) (bool, error) {
relPath, err := filepath.Rel(rootPath, filePath)
if err != nil {
Expand Down Expand Up @@ -267,21 +177,3 @@ func matchesGlob(rootPath, filePath string, patterns []string) (bool, error) {

return false, nil
}

type debugLog struct {
enabled bool
}

func newDebugLog(enabled bool) *debugLog {
return &debugLog{
enabled: enabled,
}
}

func (d *debugLog) print(message string) {
if !d.enabled {
return
}

log.Println(message)
}
19 changes: 10 additions & 9 deletions cora_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"testing"

"github.com/shaharia-lab/cora/pkg/concatenator"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -26,9 +27,9 @@ func TestConcatenator(t *testing.T) {
require.NoError(t, err)
}

debugLog := newDebugLog(false)
concatenator := newConcatenator(outputFile, "\n---\n", "File: ", debugLog)
err := concatenator.concatenate(inputFiles)
debugLog := concatenator.NewDebugLog(false)
conc := concatenator.NewConcatenation(outputFile, "\n---\n", "File: ", debugLog)
err := conc.Concatenate(inputFiles)
require.NoError(t, err)

content, err := os.ReadFile(outputFile)
Expand Down Expand Up @@ -60,9 +61,9 @@ func TestConcatenatorLargeFiles(t *testing.T) {
require.NoError(t, err)
}

debugLog := newDebugLog(false)
concatenator := newConcatenator(outputFile, "\n", "File: ", debugLog)
err := concatenator.concatenate(inputFiles)
debugLog := concatenator.NewDebugLog(false)
conc := concatenator.NewConcatenation(outputFile, "\n", "File: ", debugLog)
err := conc.Concatenate(inputFiles)
require.NoError(t, err)

stat, err := os.Stat(outputFile)
Expand All @@ -85,7 +86,7 @@ func TestWalker(t *testing.T) {
root := t.TempDir()
createTestFiles(t, root)

debugLog := newDebugLog(false)
debugLog := concatenator.NewDebugLog(false)

tests := []struct {
name string
Expand Down Expand Up @@ -207,14 +208,14 @@ func TestDebugLog(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
debugLog := newDebugLog(tt.enabled)
debugLog := concatenator.NewDebugLog(tt.enabled)

// Capture log output
var buf bytes.Buffer
log.SetOutput(&buf)
defer log.SetOutput(os.Stderr)

debugLog.print(tt.message)
debugLog.Print(tt.message)

if tt.enabled {
assert.Contains(t, buf.String(), tt.message)
Expand Down
119 changes: 119 additions & 0 deletions pkg/concatenator/concatenator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package concatenator

import (
"bufio"
"fmt"
"io"
"log"
"os"
"path/filepath"
)

const (
defaultBufferSize = 64 * 1024 // 64KB
)

// Concatenator concatenates files into a single file.
type Concatenator struct {
outputPath string
separator []byte
debugLog *DebugLog
pathPrefix []byte
}

// NewConcatenation creates a new Concatenator.
func NewConcatenation(outputPath, separator, pathPrefix string, debugLog *DebugLog) *Concatenator {
return &Concatenator{
outputPath: outputPath,
separator: []byte(separator),
pathPrefix: []byte(pathPrefix),
debugLog: debugLog,
}
}

// Concatenate concatenates the files specified by filePaths into a single file.
func (c *Concatenator) Concatenate(filePaths []string) error {
if err := os.MkdirAll(filepath.Dir(c.outputPath), 0755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}

outFile, err := os.Create(c.outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer outFile.Close()

writer := bufio.NewWriterSize(outFile, defaultBufferSize)
defer writer.Flush()

for i, filePath := range filePaths {
if i > 0 {
if _, err := writer.Write(c.separator); err != nil {
return fmt.Errorf("failed to write separator: %w", err)
}
}

if err := c.writeFileHeader(writer, filePath); err != nil {
return err
}

if err := c.appendFileContent(writer, filePath); err != nil {
return err
}

if _, err := writer.Write([]byte{'\n'}); err != nil {
return fmt.Errorf("failed to write newline after file content: %w", err)
}
}

return nil
}

func (c *Concatenator) appendFileContent(writer *bufio.Writer, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", filePath, err)
}
defer file.Close()

_, err = io.Copy(writer, file)
if err != nil {
return fmt.Errorf("failed to copy content from %s: %w", filePath, err)
}

return nil
}

func (c *Concatenator) writeFileHeader(writer *bufio.Writer, filePath string) error {
if _, err := writer.Write(c.pathPrefix); err != nil {
return fmt.Errorf("failed to write path prefix: %w", err)
}

if _, err := writer.WriteString(filePath); err != nil {
return fmt.Errorf("failed to write file path: %w", err)
}

if _, err := writer.Write([]byte{'\n'}); err != nil {
return fmt.Errorf("failed to write newline after file path: %w", err)
}

return nil
}

type DebugLog struct {
enabled bool
}

func NewDebugLog(enabled bool) *DebugLog {
return &DebugLog{
enabled: enabled,
}
}

func (d *DebugLog) Print(message string) {
if !d.enabled {
return
}

log.Println(message)
}

0 comments on commit 88c4241

Please sign in to comment.