Skip to content

feat: generics support#8

Closed
thejoeejoee wants to merge 2 commits into
MasterOfBinary:masterfrom
thejoeejoee:feat-generics
Closed

feat: generics support#8
thejoeejoee wants to merge 2 commits into
MasterOfBinary:masterfrom
thejoeejoee:feat-generics

Conversation

@thejoeejoee

Copy link
Copy Markdown

introduced support for native golang generics

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c64304a..0aae4e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

 Note: This project is in early development. The API may change without warning in any 0.x version.

+## [Unreleased]
+
+### Changed
+
+- Introduced golang generics support for entire library Batch, Source, and Processor interfaces
+- Increased Go version requirement to 1.18
+
 ## [0.1.1] - 2024-07-18

 ### Changed
diff --git a/batch/batch.go b/batch/batch.go
index 453aab7..4adbb1d 100644
--- a/batch/batch.go
+++ b/batch/batch.go
@@ -14,7 +14,7 @@
 // to prioritize the parameters in some way. They are prioritized as follows
 // (with EOF signifying the end of the input data):
 //
-//    MaxTime = MaxItems > EOF > MinTime > MinItems
+//	MaxTime = MaxItems > EOF > MinTime > MinItems
 //
 // A few examples:
 //
@@ -22,7 +22,7 @@
 // processed right away.
 //
 // MinItems = 10, MinTime = 2s. After 1s, 10 items have been read. They are
-// not processed until 2s has passed (along with all other items that have
+// not processed until 2s has passed (along with all other in that have
 // been read up to the 2s mark).
 //
 // MaxItems = 10, MinTime = 2s. After 1s, 10 items have been read. They aren't
@@ -48,10 +48,10 @@ import (
 // To create a new Batch, call the New function. Creating one using &Batch{}
 // will return the default Batch.
 //
-//    // The following are equivalent
-//    defaultBatch1 := &batch.Batch{}
-//    defaultBatch2 := batch.New(nil)
-//    defaultBatch3 := batch.New(batch.NewConstantConfig(&batch.ConfigValues{}))
+//	// The following are equivalent
+//	defaultBatch1 := &batch.Batch{}
+//	defaultBatch2 := batch.New(nil)
+//	defaultBatch3 := batch.New(batch.NewConstantConfig(&batch.ConfigValues{}))
 //
 // The defaults (with nil Config) provide a usable, but likely suboptimal, Batch
 // where items are processed as soon as they are retrieved from the source.
@@ -63,7 +63,7 @@ import (
 // by Batch. For easier usage, the helper function NextItem can be used to
 // read from the input channel, set the data, and return the modified Item:
 //
-//    ps.Output() <- batch.NextItem(ps, item)
+//	ps.Output() <- batch.NextItem(ps, item)
 //
 // Batch runs asynchronously until the source closes its PipelineSource, signaling
 // that there is nothing else to read. Once that happens, and the pipeline has
@@ -74,34 +74,36 @@ import (
 // The first way can be used if errors need to be processed elsewhere. A simple
 // loop could look like this:
 //
-//    errs := myBatch.Go(ctx, s, p)
-//    for err := range errs {
-//      // Log the error here...
-//      log.Print(err.Error())
-//    }
-//    // Now batch processing is done
+//	errs := myBatch.Go(ctx, s, p)
+//	for err := range errs {
+//	  // Log the error here...
+//	  log.Print(err.Error())
+//	}
+//	// Now batch processing is done
 //
 // If the errors don't need to be processed, the IgnoreErrors function can be
 // used to drain the error channel. Then the Done channel can be used to
 // determine whether or not batch processing is complete:
 //
-//    batch.IgnoreErrors(myBatch.Go(ctx, s, p))
-//    <-myBatch.Done()
-//    // Now batch processing is done
+//	batch.IgnoreErrors(myBatch.Go(ctx, s, p))
+//	<-myBatch.Done()
+//	// Now batch processing is done
 //
 // Note that the errors returned on the error channel may be wrapped in a
 // batch.Error so the caller knows whether they come from the source or the
 // processor (or neither). Errors from the source will be of type SourceError,
 // and errors from the processor will be of type ProcessorError. Errors from
 // Batch itself will be neither.
-type Batch struct {
+type Batch[I any, O any] struct {
 	config Config

-	src   Source
-	proc  Processor
-	items chan *Item
-	ids   chan uint64 // For unique IDs
-	done  chan struct{}
+	src  Source[I, I]
+	proc Processor[I, O]
+	in   chan *Item[I]
+	out  chan *Item[O]
+
+	ids  chan uint64 // For unique IDs
+	done chan struct{}

 	// mu protects the following variables. The reason errs is protected is
 	// to avoid sending on a closed channel in the Go method.
@@ -116,15 +118,15 @@ type Batch struct {
 // To avoid race conditions, the config cannot be changed after the Batch
 // is created. Instead, implement the Config interface to support changing
 // values.
-func New(config Config) *Batch {
-	return &Batch{
+func New[I any, O any](config Config) *Batch[I, O] {
+	return &Batch[I, O]{
 		config: config,
 	}
 }

 // Source reads items that are to be batch processed.
-type Source interface {
-	// Read reads items from somewhere and writes them to the Output
+type Source[I any, O any] interface {
+	// Read reads in from somewhere and writes them to the Output
 	// channel of ps. Any errors it encounters while reading are written to the
 	// Errors channel. The Input channel provides a steady stream of Items that
 	// have pre-set metadata so the batch processor can identify them. A helper
@@ -147,12 +149,12 @@ type Source interface {
 	//    }
 	//
 	// Read should not modify an item after adding it to items.
-	Read(ctx context.Context, ps *PipelineStage)
+	Read(ctx context.Context, ps *PipelineStage[I, O])
 }

 // Processor processes items in batches.
-type Processor interface {
-	// Process processes items from ps's Input channel and returns any errors
+type Processor[I any, O any] interface {
+	// Process processes in from ps's Input channel and returns any errors
 	// encountered on the Errors channel. When it is done, it must close ps
 	// to signify that it's finished processing. Simply returning isn't enough.
 	//
@@ -189,7 +191,7 @@ type Processor interface {
 	// Process may be run in any number of concurrent goroutines. If
 	// concurrency needs to be limited it must be done in Process; for
 	// example, by using a semaphore channel.
-	Process(ctx context.Context, ps *PipelineStage)
+	Process(ctx context.Context, ps *PipelineStage[I, O])
 }

 // Go starts batch processing asynchronously and returns a channel on
@@ -200,9 +202,9 @@ type Processor interface {
 // calls to Go are not allowed. If Go is called before a previous call
 // completes, the second one will panic.
 //
-//    // NOTE: bad - this will panic!
-//    errs := batch.Go(ctx, s, p)
-//    errs2 := batch.Go(ctx, s, p) // this call panics
+//	// NOTE: bad - this will panic!
+//	errs := batch.Go(ctx, s, p)
+//	errs2 := batch.Go(ctx, s, p) // this call panics
 //
 // Note that Go does not stop if ctx is done. Otherwise loss of data could occur.
 // Suppose the source reads item A and then ctx is canceled. If Go were to return
@@ -215,7 +217,7 @@ type Processor interface {
 // done, it closes its error channel to signal to the batch processor.
 // Finally, the batch processor signals to its caller that processing is
 // complete and the entire pipeline is drained.
-func (b *Batch) Go(ctx context.Context, s Source, p Processor) <-chan error {
+func (b *Batch[I, O]) Go(ctx context.Context, s Source[I, I], p Processor[I, O]) <-chan error {
 	b.mu.Lock()
 	defer b.mu.Unlock()

@@ -232,7 +234,8 @@ func (b *Batch) Go(ctx context.Context, s Source, p Processor) <-chan error {

 	b.src = s
 	b.proc = p
-	b.items = make(chan *Item)
+	b.in = make(chan *Item[I])
+	b.out = make(chan *Item[O])
 	b.ids = make(chan uint64)
 	b.done = make(chan struct{})

@@ -246,12 +249,12 @@ func (b *Batch) Go(ctx context.Context, s Source, p Processor) <-chan error {
 // Done provides an alternative way to determine when processing is
 // complete. When it is, the channel is closed, signaling that everything
 // is done.
-func (b *Batch) Done() <-chan struct{} {
+func (b *Batch[I, O]) Done() <-chan struct{} {
 	return b.done
 }

-// doIDGenerator generates unique IDs for the items in the pipeline.
-func (b *Batch) doIDGenerator() {
+// doIDGenerator generates unique IDs for the in in the pipeline.
+func (b *Batch[I, O]) doIDGenerator() {
 	for id := uint64(0); ; id++ {
 		select {
 		case b.ids <- id:
@@ -263,11 +266,11 @@ func (b *Batch) doIDGenerator() {
 }

 // doReader starts the reader goroutine and reads from its channels.
-func (b *Batch) doReader(ctx context.Context) {
-	in := make(chan *Item)
-	out := make(chan *Item)
+func (b *Batch[I, O]) doReader(ctx context.Context) {
+	in := make(chan *Item[I])
+	out := make(chan *Item[I])
 	errs := make(chan error)
-	ps := &PipelineStage{
+	ps := &PipelineStage[I, I]{
 		Input:  in,
 		Output: out,
 		Errors: errs,
@@ -275,7 +278,7 @@ func (b *Batch) doReader(ctx context.Context) {

 	go b.src.Read(ctx, ps)

-	nextItem := &Item{
+	nextItem := &Item[I]{
 		id: <-b.ids,
 	}

@@ -283,13 +286,13 @@ func (b *Batch) doReader(ctx context.Context) {
 	for !outClosed || !errClosed {
 		select {
 		case in <- nextItem:
-			nextItem = &Item{
+			nextItem = &Item[I]{
 				id: <-b.ids,
 			}

 		case item, ok := <-out:
 			if ok {
-				b.items <- item
+				b.in <- item
 			} else {
 				outClosed = true
 			}
@@ -305,11 +308,11 @@ func (b *Batch) doReader(ctx context.Context) {
 		}
 	}

-	close(b.items)
+	close(b.in)
 }

 // doProcessors starts the processor goroutine.
-func (b *Batch) doProcessors(ctx context.Context) {
+func (b *Batch[I, O]) doProcessors(ctx context.Context) {
 	var wg sync.WaitGroup
 	wg.Add(1)
 	go func() {
@@ -336,7 +339,7 @@ func fixConfig(c ConfigValues) ConfigValues {
 	return c
 }

-func (b *Batch) process(ctx context.Context) {
+func (b *Batch[I, O]) process(ctx context.Context) {
 	var (
 		wg      sync.WaitGroup
 		done    bool
@@ -356,10 +359,10 @@ func (b *Batch) process(ctx context.Context) {
 			bufSize = 1024
 		}

-		var items = make([]*Item, 0, bufSize)
+		var items = make([]*Item[I], 0, bufSize)
 		done, items = b.waitForItems(ctx, items, &config)

-		// TODO this resets the time whenever no items are available. Need to
+		// TODO this resets the time whenever no in are available. Need to
 		// decide if that's the right way to do it.
 		if len(items) == 0 {
 			continue
@@ -370,10 +373,10 @@ func (b *Batch) process(ctx context.Context) {
 		go func() {
 			defer wg.Done()

-			in := make(chan *Item)
-			out := make(chan *Item)
+			in := make(chan *Item[I])
+			out := make(chan *Item[O])
 			errs := make(chan error)
-			ps := &PipelineStage{
+			ps := &PipelineStage[I, O]{
 				Input:  in,
 				Output: out,
 				Errors: errs,
@@ -393,7 +396,7 @@ func (b *Batch) process(ctx context.Context) {
 				select {
 				case item, ok := <-out:
 					if ok {
-						b.items <- item
+						b.out <- item
 					} else {
 						outClosed = true
 					}
@@ -418,7 +421,7 @@ func (b *Batch) process(ctx context.Context) {
 // waitForItems waits until enough items are read to begin batch processing, based
 // on config. It returns true if processing is completely finished, and false
 // otherwise.
-func (b *Batch) waitForItems(ctx context.Context, items []*Item, config *ConfigValues) (bool, []*Item) {
+func (b *Batch[I, O]) waitForItems(ctx context.Context, items []*Item[I], config *ConfigValues) (bool, []*Item[I]) {
 	var (
 		reachedMinTime bool
 		itemsRead      uint64
@@ -445,7 +448,7 @@ func (b *Batch) waitForItems(ctx context.Context, items []*Item, config *ConfigV

 	for {
 		select {
-		case item, ok := <-b.items:
+		case item, ok := <-b.in:
 			if ok {
 				items = append(items, item)
 				itemsRead++
diff --git a/batch/batch_test.go b/batch/batch_test.go
index 8998386..ec69267 100644
--- a/batch/batch_test.go
+++ b/batch/batch_test.go
@@ -14,16 +14,16 @@ import (
 )

 type sourceFromSlice struct {
-	slice    []interface{}
+	slice    []int
 	duration time.Duration
 }

-func (s *sourceFromSlice) Read(ctx context.Context, ps *PipelineStage) {
+func (s *sourceFromSlice) Read(ctx context.Context, ps *PipelineStage[int, int]) {
 	defer ps.Close()

 	for _, item := range s.slice {
 		time.Sleep(s.duration)
-		ps.Output <- NextItem(ps, item)
+		ps.Output <- NextItem[int, int](ps, item)
 	}
 }

@@ -32,7 +32,7 @@ type processorCounter struct {
 	num        uint32
 }

-func (p *processorCounter) Process(ctx context.Context, ps *PipelineStage) {
+func (p *processorCounter) Process(ctx context.Context, ps *PipelineStage[int, int]) {
 	defer ps.Close()

 	count := 0
@@ -62,11 +62,11 @@ func TestBatch_Go(t *testing.T) {
 	t.Run("basic test", func(t *testing.T) {
 		t.Parallel()

-		batch := &Batch{}
+		batch := &Batch[int, any]{}
 		s := &sourceFromSlice{
-			slice: []interface{}{1, 2, 3, 4, 5},
+			slice: []int{1, 2, 3, 4, 5},
 		}
-		p := &processor.Nil{}
+		p := &processor.Nil[int, any]{}

 		errs := batch.Go(context.Background(), s, p)

@@ -86,11 +86,11 @@ func TestBatch_Go(t *testing.T) {
 		t.Parallel()

 		// Concurrent calls to Go should panic
-		batch := &Batch{}
-		s := &source.Nil{
+		batch := &Batch[int, any]{}
+		s := &source.Nil[int]{
 			Duration: time.Second,
 		}
-		p := &processor.Nil{
+		p := &processor.Nil[int, any]{
 			Duration: 0,
 		}

@@ -116,11 +116,11 @@ func TestBatch_Go(t *testing.T) {
 		t.Parallel()

 		errSrc := errors.New("source")
-		batch := &Batch{}
-		s := &source.Error{
+		batch := &Batch[int, any]{}
+		s := &source.Error[int]{
 			Err: errSrc,
 		}
-		p := &processor.Nil{}
+		p := &processor.Nil[int, any]{}

 		errs := batch.Go(context.Background(), s, p)

@@ -146,11 +146,11 @@ func TestBatch_Go(t *testing.T) {
 		t.Parallel()

 		errProc := errors.New("processor")
-		batch := &Batch{}
+		batch := &Batch[int, any]{}
 		s := &sourceFromSlice{
-			slice: []interface{}{1},
+			slice: []int{1},
 		}
-		p := &processor.Error{
+		p := &processor.Error[int, any]{
 			Err: errProc,
 		}

@@ -297,12 +297,12 @@ func TestBatch_Go(t *testing.T) {
 			t.Run(test.name, func(t *testing.T) {
 				t.Parallel()

-				inputSlice := make([]interface{}, test.inputSize)
+				inputSlice := make([]int, test.inputSize)
 				for i := 0; i < len(inputSlice); i++ {
 					inputSlice[i] = rand.Int()
 				}

-				batch := New(NewConstantConfig(test.config))
+				batch := New[int, int](NewConstantConfig(test.config))
 				s := &sourceFromSlice{
 					slice:    inputSlice,
 					duration: test.inputDuration,
@@ -326,11 +326,11 @@ func TestBatch_Done(t *testing.T) {
 	t.Run("basic test", func(t *testing.T) {
 		t.Parallel()

-		batch := &Batch{}
-		s := &source.Nil{
+		batch := &Batch[int, any]{}
+		s := &source.Nil[int]{
 			Duration: 0,
 		}
-		p := &processor.Nil{
+		p := &processor.Nil[int, any]{
 			Duration: 0,
 		}

@@ -347,12 +347,12 @@ func TestBatch_Done(t *testing.T) {
 	t.Run("with source sleep", func(t *testing.T) {
 		t.Parallel()

-		batch := &Batch{}
+		batch := &Batch[int, any]{}
 		s := &sourceFromSlice{
-			slice:    []interface{}{1},
+			slice:    []int{1},
 			duration: 100 * time.Millisecond,
 		}
-		p := &processor.Nil{
+		p := &processor.Nil[int, any]{
 			Duration: 10 * time.Millisecond,
 		}

@@ -372,12 +372,12 @@ func TestBatch_Done(t *testing.T) {
 	t.Run("with processor sleep", func(t *testing.T) {
 		t.Parallel()

-		batch := &Batch{}
+		batch := &Batch[int, any]{}
 		s := &sourceFromSlice{
-			slice:    []interface{}{1},
+			slice:    []int{1},
 			duration: 10 * time.Millisecond,
 		}
-		p := &processor.Nil{
+		p := &processor.Nil[int, any]{
 			Duration: 100 * time.Millisecond,
 		}

diff --git a/batch/example_test.go b/batch/example_test.go
index 9e9b4a9..4b10d46 100644
--- a/batch/example_test.go
+++ b/batch/example_test.go
@@ -13,17 +13,19 @@ import (
 // printProcessor is a Processor that prints items in batches.
 //
 // To demonstrate how errors can be handled, it fails to process the number 5.
-type printProcessor struct{}
+type printProcessor[I, O any] struct{}

 // Process prints a batch of items.
-func (p printProcessor) Process(ctx context.Context, ps *batch.PipelineStage) {
+func (p printProcessor[I, O]) Process(ctx context.Context, ps *batch.PipelineStage[int, any]) {
 	// Process needs to close ps after it's done
 	defer ps.Close()

-	toPrint := make([]interface{}, 0, 5)
+	var errValue = 5
+
+	toPrint := make([]int, 0, 5)
 	for item := range ps.Input {
 		// Get returns the item itself
-		if item.Get() == 5 {
+		if item.Get() == errValue {
 			ps.Errors <- errors.New("cannot process 5")
 			continue
 		}
@@ -39,12 +41,12 @@ func Example() {
 	config := batch.NewConstantConfig(&batch.ConfigValues{
 		MinItems: 5,
 	})
-	b := batch.New(config)
-	p := &printProcessor{}
+	b := batch.New[int, any](config)
+	p := &printProcessor[int, any]{}

 	// Channel is a Source that reads from a channel until it's closed
-	ch := make(chan interface{})
-	s := source.Channel{
+	ch := make(chan int)
+	s := source.Channel[int]{
 		Input: ch,
 	}

diff --git a/batch/helpers.go b/batch/helpers.go
index f57420e..b90db9a 100644
--- a/batch/helpers.go
+++ b/batch/helpers.go
@@ -4,12 +4,12 @@ package batch
 // its data, and returns it. If the input channel is closed, it returns nil.
 // NextItem can be used in the source Read function:
 //
-//    func (s *source) Read(ctx context.Context, ps batch.PipelineStage) {
-//      // Read data into myData...
-//      items <- batch.NextItem(ps, myData)
-//      // ...
-//    }
-func NextItem(ps *PipelineStage, data interface{}) *Item {
+//	func (s *source) Read(ctx context.Context, ps batch.PipelineStage) {
+//	  // Read data into myData...
+//	  in <- batch.NextItem(ps, myData)
+//	  // ...
+//	}
+func NextItem[I, O any](ps *PipelineStage[I, O], data I) *Item[I] {
 	i, ok := <-ps.Input
 	if !ok {
 		return nil
@@ -22,12 +22,12 @@ func NextItem(ps *PipelineStage, data interface{}) *Item {
 // It can be used with Batch.Go if errors aren't needed. Since the error channel
 // is unbuffered, one cannot just throw away the error channel like this:
 //
-//    // NOTE: bad - this can cause a deadlock!
-//    _ = batch.Go(ctx, p, s)
+//	// NOTE: bad - this can cause a deadlock!
+//	_ = batch.Go(ctx, p, s)
 //
 // Instead, IgnoreErrors can be used to safely throw away all errors:
 //
-//    batch.IgnoreErrors(myBatch.Go(ctx, p, s))
+//	batch.IgnoreErrors(myBatch.Go(ctx, p, s))
 func IgnoreErrors(errs <-chan error) {
 	// nil channels always block, so check for nil first to avoid a goroutine
 	// leak
diff --git a/batch/helpers_test.go b/batch/helpers_test.go
index 34bc6ed..f306fb1 100644
--- a/batch/helpers_test.go
+++ b/batch/helpers_test.go
@@ -9,15 +9,15 @@ import (

 func TestNextItem(t *testing.T) {
 	t.Run("with item", func(t *testing.T) {
-		ch := make(chan *Item)
+		ch := make(chan *Item[any])

 		go func() {
-			ch <- &Item{
+			ch <- &Item[any]{
 				id: 100,
 			}
 		}()

-		ps := &PipelineStage{
+		ps := &PipelineStage[any, any]{
 			Input: ch,
 		}

@@ -31,10 +31,10 @@ func TestNextItem(t *testing.T) {
 	})

 	t.Run("closed channel", func(t *testing.T) {
-		ch := make(chan *Item)
+		ch := make(chan *Item[any])
 		close(ch)

-		ps := &PipelineStage{
+		ps := &PipelineStage[any, any]{
 			Input: ch,
 		}

diff --git a/batch/item.go b/batch/item.go
index 7ea5f00..d4e0614 100644
--- a/batch/item.go
+++ b/batch/item.go
@@ -3,28 +3,28 @@ package batch
 import "sync"

 // Item holds a single item in the batch processing pipeline.
-type Item struct {
+type Item[T any] struct {
 	mu   sync.RWMutex
 	id   uint64
-	item interface{}
+	item T
 }

 // GetID returns a unique ID of the current item in the pipeline.
-func (i *Item) GetID() uint64 {
+func (i *Item[T]) GetID() uint64 {
 	i.mu.RLock()
 	defer i.mu.RUnlock()
 	return i.id
 }

 // Get returns the item data.
-func (i *Item) Get() interface{} {
+func (i *Item[T]) Get() T {
 	i.mu.RLock()
 	defer i.mu.RUnlock()
 	return i.item
 }

 // Set sets the item data.
-func (i *Item) Set(item interface{}) {
+func (i *Item[T]) Set(item T) {
 	i.mu.Lock()
 	defer i.mu.Unlock()
 	i.item = item
diff --git a/batch/item_mock.go b/batch/item_mock.go
index e760a6e..cb22eb3 100644
--- a/batch/item_mock.go
+++ b/batch/item_mock.go
@@ -8,7 +8,7 @@ import "sync"
 type MockItemGenerator struct {
 	closeOnce sync.Once
 	done      chan struct{}
-	ch        chan *Item
+	ch        chan *Item[int]
 }

 // NewMockItemGenerator returns a new MockItemGenerator.
@@ -17,19 +17,19 @@ type MockItemGenerator struct {
 func NewMockItemGenerator() *MockItemGenerator {
 	m := &MockItemGenerator{
 		done: make(chan struct{}),
-		ch:   make(chan *Item),
+		ch:   make(chan *Item[int]),
 	}

 	go func() {
 		id := uint64(0)
-		nextItem := &Item{
+		nextItem := &Item[int]{
 			id: id,
 		}
 		for {
 			select {
 			case m.ch <- nextItem:
 				id++
-				nextItem = &Item{
+				nextItem = &Item[int]{
 					id: id,
 				}

@@ -50,6 +50,6 @@ func (m *MockItemGenerator) Close() {
 }

 // GetCh returns a channel of Items with unique IDs.
-func (m *MockItemGenerator) GetCh() <-chan *Item {
+func (m *MockItemGenerator) GetCh() <-chan *Item[int] {
 	return m.ch
 }
diff --git a/batch/pipeline_stage.go b/batch/pipeline_stage.go
index 228c5a7..296c2ba 100644
--- a/batch/pipeline_stage.go
+++ b/batch/pipeline_stage.go
@@ -2,12 +2,12 @@ package batch

 // PipelineStage contains the input and output channels for a single
 // stage of the batch pipeline.
-type PipelineStage struct {
-	// Input contains the input items for a pipeline stage.
-	Input <-chan *Item
+type PipelineStage[I any, O any] struct {
+	// Input contains the input in for a pipeline stage.
+	Input <-chan *Item[I]

 	// Output is for the output of the pipeline stage.
-	Output chan<- *Item
+	Output chan<- *Item[O]

 	//Retry chan<- *Item

@@ -19,7 +19,7 @@ type PipelineStage struct {
 //
 // Note that it will also close the write channels. Do not close them separately
 // or it will panic.
-func (p *PipelineStage) Close() {
+func (p *PipelineStage[T, O]) Close() {
 	close(p.Output)
 	close(p.Errors)
 }
diff --git a/batch/pipeline_stage_test.go b/batch/pipeline_stage_test.go
index 0740e30..1445247 100644
--- a/batch/pipeline_stage_test.go
+++ b/batch/pipeline_stage_test.go
@@ -3,9 +3,9 @@ package batch
 import "testing"

 func TestPipelineStage_Close(t *testing.T) {
-	out := make(chan *Item)
+	out := make(chan *Item[any])
 	errs := make(chan error)
-	ps := &PipelineStage{
+	ps := &PipelineStage[any, any]{
 		Output: out,
 		Errors: errs,
 	}
diff --git a/go.mod b/go.mod
index 7e85fd7..f9272dd 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
 module github.com/MasterOfBinary/gobatch

-go 1.11
+go 1.18
diff --git a/processor/error.go b/processor/error.go
index 273b8e9..2955d5e 100644
--- a/processor/error.go
+++ b/processor/error.go
@@ -7,12 +7,12 @@ import (
 )

 // Error returns a Processor that returns an error while processing.
-type Error struct {
+type Error[I, O any] struct {
 	Err error
 }

 // Process discards all data sent to it after a certain amount of time.
-func (p *Error) Process(ctx context.Context, ps *batch.PipelineStage) {
+func (p *Error[I, O]) Process(ctx context.Context, ps *batch.PipelineStage[I, O]) {
 	ps.Errors <- p.Err
 	ps.Close()
 }
diff --git a/processor/nil.go b/processor/nil.go
index ef6b66e..cb8265f 100644
--- a/processor/nil.go
+++ b/processor/nil.go
@@ -9,12 +9,12 @@ import (

 // Nil is a Processor that discards all data after a specified duration.
 // It can be used as a mock Processor.
-type Nil struct {
+type Nil[I, O any] struct {
 	Duration time.Duration
 }

 // Process discards all data sent to it after a certain amount of time.
-func (p *Nil) Process(ctx context.Context, ps *batch.PipelineStage) {
+func (p *Nil[I, O]) Process(ctx context.Context, ps *batch.PipelineStage[I, O]) {
 	time.Sleep(p.Duration)
 	ps.Close()
 }
diff --git a/source/channel.go b/source/channel.go
index 53cd536..bffb06a 100644
--- a/source/channel.go
+++ b/source/channel.go
@@ -8,12 +8,12 @@ import (

 // Channel is a Source that reads from input until it is closed.
 // The input channel can be buffered or unbuffered.
-type Channel struct {
-	Input <-chan interface{}
+type Channel[I any] struct {
+	Input <-chan I
 }

 // Read reads from items until the input channel is closed.
-func (s *Channel) Read(ctx context.Context, ps* batch.PipelineStage) {
+func (s *Channel[I]) Read(ctx context.Context, ps *batch.PipelineStage[I, I]) {
 	defer ps.Close()

 	out := ps.Output
diff --git a/source/channel_test.go b/source/channel_test.go
index 505ab15..6179fb4 100644
--- a/source/channel_test.go
+++ b/source/channel_test.go
@@ -18,14 +18,14 @@ func TestChannelSource_Read(t *testing.T) {
 	var wg sync.WaitGroup
 	defer wg.Wait()

-	in := make(chan interface{}, size)
-	items := make(chan *batch.Item)
+	in := make(chan any, size)
+	items := make(chan *batch.Item[any])
 	_ = make(chan error)

 	itemGen := batch.NewMockItemGenerator()
 	defer itemGen.Close()

-	s := Channel{
+	s := Channel[any]{
 		Input: in,
 	}

diff --git a/source/error.go b/source/error.go
index de00bc9..8ab36e4 100644
--- a/source/error.go
+++ b/source/error.go
@@ -8,12 +8,12 @@ import (

 // Error is a Source that returns an error and then closes immediately.
 // It can be used as a mock Source.
-type Error struct {
+type Error[I any] struct {
 	Err error
 }

 // Read returns an error and then closes.
-func (s *Error) Read(ctx context.Context, ps *batch.PipelineStage) {
+func (s *Error[I]) Read(ctx context.Context, ps *batch.PipelineStage[I, I]) {
 	ps.Errors <- s.Err
 	ps.Close()
 }
diff --git a/source/nil.go b/source/nil.go
index cdf85c0..23ad5e4 100644
--- a/source/nil.go
+++ b/source/nil.go
@@ -10,12 +10,12 @@ import (
 // Nil is a Source that doesn't read any data. Instead it closes the
 // pipeline stage after specified duration. It can be used as a mock
 // Source.
-type Nil struct {
+type Nil[I any] struct {
 	Duration time.Duration
 }

 // Read doesn't read anything.
-func (s *Nil) Read(ctx context.Context, ps *batch.PipelineStage) {
+func (s *Nil[I]) Read(ctx context.Context, ps *batch.PipelineStage[I, I]) {
 	time.Sleep(s.Duration)
 	ps.Close()
 }
diff --git a/batch/batch.go b/batch/batch.go
index 4adbb1d..675959d 100644
--- a/batch/batch.go
+++ b/batch/batch.go
@@ -421,7 +421,7 @@ func (b *Batch[I, O]) process(ctx context.Context) {
 // waitForItems waits until enough items are read to begin batch processing, based
 // on config. It returns true if processing is completely finished, and false
 // otherwise.
-func (b *Batch[I, O]) waitForItems(ctx context.Context, items []*Item[I], config *ConfigValues) (bool, []*Item[I]) {
+func (b *Batch[I, O]) waitForItems(_ context.Context, items []*Item[I], config *ConfigValues) (bool, []*Item[I]) {
 	var (
 		reachedMinTime bool
 		itemsRead      uint64
diff --git a/batch/batch_test.go b/batch/batch_test.go
index ec69267..2af1c91 100644
--- a/batch/batch_test.go
+++ b/batch/batch_test.go
@@ -18,7 +18,7 @@ type sourceFromSlice struct {
 	duration time.Duration
 }

-func (s *sourceFromSlice) Read(ctx context.Context, ps *PipelineStage[int, int]) {
+func (s *sourceFromSlice) Read(_ context.Context, ps *PipelineStage[int, int]) {
 	defer ps.Close()

 	for _, item := range s.slice {
@@ -32,7 +32,7 @@ type processorCounter struct {
 	num        uint32
 }

-func (p *processorCounter) Process(ctx context.Context, ps *PipelineStage[int, int]) {
+func (p *processorCounter) Process(_ context.Context, ps *PipelineStage[int, int]) {
 	defer ps.Close()

 	count := 0
@@ -126,14 +126,13 @@ func TestBatch_Go(t *testing.T) {

 		var found bool
 		for err := range errs {
-			if src, ok := err.(*SourceError); ok {
-				if src.Original() == errSrc {
+			var src *SourceError
+			if errors.As(err, &src) {
+				if errors.Is(errSrc, src.Original()) {
 					found = true
 				} else {
 					t.Errorf("Found source error %v, want %v", src.Original(), errSrc)
 				}
-			} else {
-				t.Error("Found an unexpected error")
 			}
 		}

@@ -158,14 +157,13 @@ func TestBatch_Go(t *testing.T) {

 		var found bool
 		for err := range errs {
-			if proc, ok := err.(*ProcessorError); ok {
-				if proc.Original() == errProc {
+			var proc *ProcessorError
+			if errors.As(err, &proc) {
+				if errors.Is(errProc, proc.Original()) {
 					found = true
 				} else {
 					t.Errorf("Found processor error %v, want %v", proc.Original(), errProc)
 				}
-			} else {
-				t.Error("Found an unexpected error")
 			}
 		}

diff --git a/batch/example_test.go b/batch/example_test.go
index 4b10d46..721445d 100644
--- a/batch/example_test.go
+++ b/batch/example_test.go
@@ -16,7 +16,7 @@ import (
 type printProcessor[I, O any] struct{}

 // Process prints a batch of items.
-func (p printProcessor[I, O]) Process(ctx context.Context, ps *batch.PipelineStage[int, any]) {
+func (p printProcessor[I, O]) Process(_ context.Context, ps *batch.PipelineStage[int, any]) {
 	// Process needs to close ps after it's done
 	defer ps.Close()

diff --git a/processor/error.go b/processor/error.go
index 2955d5e..e6477dd 100644
--- a/processor/error.go
+++ b/processor/error.go
@@ -12,7 +12,7 @@ type Error[I, O any] struct {
 }

 // Process discards all data sent to it after a certain amount of time.
-func (p *Error[I, O]) Process(ctx context.Context, ps *batch.PipelineStage[I, O]) {
+func (p *Error[I, O]) Process(_ context.Context, ps *batch.PipelineStage[I, O]) {
 	ps.Errors <- p.Err
 	ps.Close()
 }
diff --git a/processor/nil.go b/processor/nil.go
index cb8265f..9e71433 100644
--- a/processor/nil.go
+++ b/processor/nil.go
@@ -14,7 +14,7 @@ type Nil[I, O any] struct {
 }

 // Process discards all data sent to it after a certain amount of time.
-func (p *Nil[I, O]) Process(ctx context.Context, ps *batch.PipelineStage[I, O]) {
+func (p *Nil[I, O]) Process(_ context.Context, ps *batch.PipelineStage[I, O]) {
 	time.Sleep(p.Duration)
 	ps.Close()
 }
diff --git a/source/channel.go b/source/channel.go
index bffb06a..8fa51e9 100644
--- a/source/channel.go
+++ b/source/channel.go
@@ -13,7 +13,7 @@ type Channel[I any] struct {
 }

 // Read reads from items until the input channel is closed.
-func (s *Channel[I]) Read(ctx context.Context, ps *batch.PipelineStage[I, I]) {
+func (s *Channel[I]) Read(_ context.Context, ps *batch.PipelineStage[I, I]) {
 	defer ps.Close()

 	out := ps.Output
diff --git a/source/error.go b/source/error.go
index 8ab36e4..56832eb 100644
--- a/source/error.go
+++ b/source/error.go
@@ -13,7 +13,7 @@ type Error[I any] struct {
 }

 // Read returns an error and then closes.
-func (s *Error[I]) Read(ctx context.Context, ps *batch.PipelineStage[I, I]) {
+func (s *Error[I]) Read(_ context.Context, ps *batch.PipelineStage[I, I]) {
 	ps.Errors <- s.Err
 	ps.Close()
 }
diff --git a/source/nil.go b/source/nil.go
index 23ad5e4..4152640 100644
--- a/source/nil.go
+++ b/source/nil.go
@@ -15,7 +15,7 @@ type Nil[I any] struct {
 }

 // Read doesn't read anything.
-func (s *Nil[I]) Read(ctx context.Context, ps *batch.PipelineStage[I, I]) {
+func (s *Nil[I]) Read(_ context.Context, ps *batch.PipelineStage[I, I]) {
 	time.Sleep(s.Duration)
 	ps.Close()
 }
@thejoeejoee thejoeejoee marked this pull request as draft March 26, 2025 14:36
@MasterOfBinary

Copy link
Copy Markdown
Owner

Closing as superseded by merged generics support in v0.5.0. Thanks for the early generics work and proposal.

MasterOfBinary added a commit that referenced this pull request May 29, 2026
Fix #8: the Nil processor was described as "passes items through
unchanged (for benchmarking)", which is wrong. Per processor/nil.go it
sleeps for a configurable Duration (and can mark items cancelled via
MarkCancelled). Corrected in CLAUDE.md, AGENTS.md, and README.md to match
processor/doc.go.

Fix #9: the engine emits wrapped errors as pointers (&SourceError{},
&ProcessorError{} in batch/batch.go), so errors.As must target the
pointer type. Made the CLAUDE.md/AGENTS.md guidance explicit with a
concrete example; README already used the pointer form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants