init: basic tools
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
cmd
|
||||
coverage.out
|
8
Makefile
Normal file
8
Makefile
Normal file
@@ -0,0 +1,8 @@
|
||||
.PHONY: coverage test
|
||||
|
||||
coverage:
|
||||
go test -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
test:
|
||||
go test -v -cover ./...
|
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module code.uint32.ru/dmitry/script
|
||||
|
||||
go 1.24.5
|
||||
|
||||
require golang.org/x/sync v0.16.0
|
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
29
input_mem.go
Normal file
29
input_mem.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
type MemReader struct {
|
||||
rows [][]string
|
||||
curr int
|
||||
}
|
||||
|
||||
func NewMemReader(records [][]string) *MemReader {
|
||||
return &MemReader{
|
||||
rows: records,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MemReader) Read(context.Context) ([]string, error) {
|
||||
if m.curr == len(m.rows) || len(m.rows) == 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
defer func() {
|
||||
m.curr++
|
||||
}()
|
||||
|
||||
return m.rows[m.curr], nil
|
||||
}
|
36
input_scv.go
Normal file
36
input_scv.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CSV creates csv.Reader reading from filename.
|
||||
func NewCSVReader(filename string) (*CSVReader, error) {
|
||||
return newCSVreader(filename)
|
||||
}
|
||||
|
||||
type CSVReader struct {
|
||||
f *os.File
|
||||
rdr *csv.Reader
|
||||
}
|
||||
|
||||
func (c *CSVReader) Read(context.Context) ([]string, error) {
|
||||
return c.rdr.Read()
|
||||
}
|
||||
|
||||
func (c *CSVReader) Close() error {
|
||||
return c.f.Close()
|
||||
}
|
||||
|
||||
func newCSVreader(name string) (*CSVReader, error) {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
csvr := csv.NewReader(f)
|
||||
|
||||
return &CSVReader{f: f, rdr: csvr}, nil
|
||||
}
|
37
input_stdin.go
Normal file
37
input_stdin.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StdinReader reads from stdin
|
||||
type StdinReader struct {
|
||||
scanner bufio.Scanner
|
||||
}
|
||||
|
||||
func NewStdinReader() *StdinReader {
|
||||
return &StdinReader{
|
||||
scanner: *bufio.NewScanner(os.Stdin),
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads from stdin until \n character
|
||||
// then splits result at "," separator and returns
|
||||
// the resulting slice.
|
||||
// It returns EOF when nothing left to read.
|
||||
func (s *StdinReader) Read(_ context.Context) ([]string, error) {
|
||||
if !s.scanner.Scan() {
|
||||
err := s.scanner.Err()
|
||||
if err == nil {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return strings.Split(s.scanner.Text(), ","), nil
|
||||
}
|
71
output_csv.go
Normal file
71
output_csv.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CSV creates csv.Writer writing to underlying file.
|
||||
// Do not forget to call Close method once you are done.
|
||||
func NewCSVWriter(filename string) (*CSVWriter, error) {
|
||||
return newCSVwriter(filename)
|
||||
}
|
||||
|
||||
type CSVWriter struct {
|
||||
f *os.File
|
||||
wr *csv.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
// Write writer row to csv. Writes are buffered
|
||||
func (c *CSVWriter) Write(_ context.Context, record []string) error {
|
||||
err := c.wr.Write(record)
|
||||
c.err = err
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Close flushes underlying csv.Writer and closes
|
||||
// file.
|
||||
func (c *CSVWriter) Close() error {
|
||||
c.wr.Flush()
|
||||
flushErr := c.wr.Error()
|
||||
closeErr := c.f.Close()
|
||||
|
||||
err := errors.Join(flushErr, closeErr)
|
||||
c.err = err
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Error return last encountered error.
|
||||
func (c *CSVWriter) Error() error {
|
||||
return c.err
|
||||
}
|
||||
|
||||
func newCSVwriter(name string) (*CSVWriter, error) {
|
||||
var (
|
||||
f *os.File
|
||||
err error
|
||||
)
|
||||
|
||||
if _, err = os.Stat(name); err == nil {
|
||||
f, err = os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0664)
|
||||
} else {
|
||||
f, err = os.Create(name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
csvwr := csv.NewWriter(f)
|
||||
|
||||
return &CSVWriter{f: f, wr: csvwr}, nil
|
||||
}
|
13
output_discard.go
Normal file
13
output_discard.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package script
|
||||
|
||||
import "context"
|
||||
|
||||
type NopWriter struct{}
|
||||
|
||||
func NewNopWriter() *NopWriter {
|
||||
return new(NopWriter)
|
||||
}
|
||||
|
||||
func (d *NopWriter) Write(context.Context, []string) error {
|
||||
return nil
|
||||
}
|
21
output_mem.go
Normal file
21
output_mem.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package script
|
||||
|
||||
import "context"
|
||||
|
||||
type MemWriter struct {
|
||||
rows [][]string
|
||||
}
|
||||
|
||||
func NewMemWriter() *MemWriter {
|
||||
return new(MemWriter)
|
||||
}
|
||||
|
||||
func (m *MemWriter) Write(_ context.Context, record []string) error {
|
||||
m.rows = append(m.rows, record)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MemWriter) Output() [][]string {
|
||||
return m.rows
|
||||
}
|
18
output_stdout.go
Normal file
18
output_stdout.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type StdoutWriter struct{}
|
||||
|
||||
func NewStdoutWriter() *StdoutWriter {
|
||||
return new(StdoutWriter)
|
||||
}
|
||||
|
||||
func (s *StdoutWriter) Write(_ context.Context, in []string) error {
|
||||
_, err := os.Stdout.WriteString(strings.Join(in, ",") + "\n")
|
||||
return err
|
||||
}
|
159
runner.go
Normal file
159
runner.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var (
|
||||
EOF error = io.EOF
|
||||
ErrNoProcessors = errors.New("no processors provided")
|
||||
)
|
||||
|
||||
var (
|
||||
_ Reader = &CSVReader{}
|
||||
_ Reader = &MemReader{}
|
||||
_ Reader = &StdinReader{}
|
||||
_ Writer = &CSVWriter{}
|
||||
_ Writer = &MemWriter{}
|
||||
_ Writer = &StdoutWriter{}
|
||||
_ Writer = &NopWriter{}
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
// Read is called by Processor to obtain input
|
||||
// for processing. Read must return EOF/io.EOF to indicate
|
||||
// that there is no more input.
|
||||
Read(context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
// Write is called by Processor to record
|
||||
// output.
|
||||
Write(context.Context, []string) error
|
||||
}
|
||||
|
||||
// Process accepts string slice, does what it should
|
||||
// and returns output. Non-nil error returned
|
||||
// by Processor terminates stops further processing.
|
||||
type Processor func(context.Context, []string) ([]string, error)
|
||||
|
||||
type RunConfig struct {
|
||||
Input Reader
|
||||
Output Writer
|
||||
Processor Processor
|
||||
Offset int
|
||||
Limit int
|
||||
Concurrency int
|
||||
}
|
||||
|
||||
// Run starts concurrency threads (goroutines), reads from provided Reader,
|
||||
// executes each Processor in the order they were provide and records result
|
||||
// with provided Writer.
|
||||
// At first Read is called offset times with output of Read being discarded.
|
||||
// Then limit Reads are made and processor is called for each portion
|
||||
// of data. If limit is 0 then Runner keep processing input until it receives
|
||||
// EOF from Reader.
|
||||
func Run(ctx context.Context, r RunConfig) error {
|
||||
if r.Concurrency == 0 {
|
||||
r.Concurrency = 1
|
||||
}
|
||||
|
||||
grp, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
rdch := make(chan []string, r.Concurrency)
|
||||
wrch := make(chan []string, r.Concurrency)
|
||||
|
||||
// read input from Reader and forward to Processor
|
||||
grp.Go(func() error {
|
||||
// closing chan for processor to complete operations
|
||||
defer close(rdch)
|
||||
|
||||
for range r.Offset {
|
||||
_, err := r.Input.Read(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not advance to required offset: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
count := 0
|
||||
for {
|
||||
inp, err := r.Input.Read(ctx)
|
||||
if err != nil && errors.Is(err, EOF) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case rdch <- inp:
|
||||
case <-ctx.Done():
|
||||
// will also close channel causing
|
||||
// all routines to complete
|
||||
return nil
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
if count == r.Limit { // will never happen if limit set to 0
|
||||
return nil
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// read output of Processor and write to Writer
|
||||
grp.Go(func() error {
|
||||
// not paying attention to context here
|
||||
// because we must complete writes
|
||||
for outp := range wrch {
|
||||
if err := r.Output.Write(ctx, outp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
grp.Go(func() error {
|
||||
// will close write chan once
|
||||
// all workers are done
|
||||
defer close(wrch)
|
||||
|
||||
workergrp, innrctx := errgroup.WithContext(ctx)
|
||||
|
||||
for range r.Concurrency {
|
||||
workergrp.Go(func() error {
|
||||
// not paying attention to context here
|
||||
// because we must complete writes
|
||||
for inp := range rdch {
|
||||
result, err := r.Processor(innrctx, inp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wrch <- result
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if err := workergrp.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := grp.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
66
runner_test.go
Normal file
66
runner_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package script_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.uint32.ru/dmitry/script"
|
||||
)
|
||||
|
||||
var echoProcessor = func(_ context.Context, in []string) ([]string, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func TestBasicRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := [][]string{
|
||||
{"hello", "world"},
|
||||
}
|
||||
|
||||
r := script.NewMemReader(input)
|
||||
w := script.NewMemWriter()
|
||||
|
||||
conf := script.RunConfig{
|
||||
Input: r,
|
||||
Output: w,
|
||||
Processor: echoProcessor,
|
||||
}
|
||||
|
||||
if err := script.Run(t.Context(), conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
output := w.Output()
|
||||
|
||||
if !reflect.DeepEqual(input, output) {
|
||||
t.Errorf("incorrect output, want: %v, got: %v", input, output)
|
||||
}
|
||||
}
|
||||
|
||||
type infiniteReader struct{}
|
||||
|
||||
func (ir *infiniteReader) Read(_ context.Context) ([]string, error) {
|
||||
return []string{"infinity", "looks", "like", "this"}, nil
|
||||
}
|
||||
|
||||
func TestRunnerObeysContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := &infiniteReader{}
|
||||
w := script.NewNopWriter()
|
||||
|
||||
conf := script.RunConfig{
|
||||
Input: r,
|
||||
Output: w,
|
||||
Processor: echoProcessor,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := script.Run(ctx, conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user