diff --git a/Makefile b/Makefile index 514d8ac..be99ee6 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ ifeq (, $(shell command -v mise)) endif @mise install +.PHONY: .lefthook # Configure git hooks for lefthook .lefthook: @lefthook install --reset-hooks-path @@ -99,7 +100,7 @@ audit: @echo "〉security audit" #@trivy fs . --format table --severity HIGH,CRITICAL @go install golang.org/x/vuln/cmd/govulncheck@latest - @go govulncheck ./... + @govulncheck ./... .PHONY: outdated ## Show outdated direct dependencies @@ -152,7 +153,7 @@ godocs: .PHONY: help ## Show help text help: - @echo "goencode\n" + @echo "\ngoencode\n" @echo "Usage:\n make [task]" @awk '{ \ if($$0 ~ /^### /){ \ diff --git a/README.md b/README.md index f69cf8a..c315b02 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ ## Features -- Generic `Codec[T]` and `StreamCodec[T]` interfaces with compile-time type safety -- Composable compression wrappers (gzip, flate, snappy, zstd, brotli) using the decorator pattern +- Generic `Codec[S, T]` and `StreamCodec[S]` function-type structs with compile-time type safety +- Type-safe codec composition via `PipeCodec` (e.g., JSON → gzip, JSON → base64) +- Standalone compression codecs (gzip, flate, snappy, zstd, brotli) as `Codec[[]byte, []byte]` - Stream support via `io.Reader`/`io.Writer` for memory-efficient pipelines - Atomic file I/O with temp file + rename - Zero dependencies in the core module @@ -24,36 +25,50 @@ go get github.com/foomo/goencode ``` -## Core Interfaces +## Core Types ```go -// Byte-oriented -type Codec[T any] interface { -Encode(v T) ([]byte, error) -Decode(b []byte, v *T) error +// Function types +type Encoder[S, T any] func(s S) (T, error) +type Decoder[S, T any] func(t T, s *S) error + +// Byte-oriented codec bundle +type Codec[S, T any] struct { + Encode Encoder[S, T] + Decode Decoder[S, T] } -// Stream-oriented -type StreamCodec[T any] interface { -Encode(w io.Writer, v T) error -Decode(r io.Reader, v *T) error +// Stream function types +type StreamEncoder[S any] func(w io.Writer, s S) error +type StreamDecoder[S any] func(r io.Reader, s *S) error + +// Stream-oriented codec bundle +type StreamCodec[S any] struct { + Encode StreamEncoder[S] + Decode StreamDecoder[S] } + +// Composition +func PipeCodec[A, B, C any](first Codec[A, B], second Codec[B, C]) Codec[A, C] ``` ## Quick Start ```go // Basic JSON encode/decode -c := json.NewCodec[User]() +c := json.NewCodec[User]() // Codec[User, []byte] b, err := c.Encode(User{Name: "Alice", Age: 30}) var u User err = c.Decode(b, &u) -// Compose with compression -c := gzip.NewCodec[User](json.NewCodec[User]()) +// Compose with compression via PipeCodec +c := goencode.PipeCodec(json.NewCodec[User](), gzip.NewCodec()) + +// Chain multiple codecs: JSON → base64 +c := goencode.PipeCodec(json.NewCodec[User](), base64.NewCodec()) // Add atomic file persistence -fc := file.NewCodec[User](gzip.NewCodec[User](json.NewCodec[User]())) +fc := file.NewCodec(goencode.PipeCodec(json.NewCodec[User](), gzip.NewCodec())) err := fc.Encode("/tmp/user.json.gz", user) ``` diff --git a/ascii85/codec.go b/ascii85/codec.go index 37543d5..f834906 100644 --- a/ascii85/codec.go +++ b/ascii85/codec.go @@ -3,26 +3,23 @@ package ascii85 import ( "bytes" stdascii85 "encoding/ascii85" -) - -// Codec is a Codec[[]byte] backed by encoding/ascii85. -// It is safe for concurrent use. -type Codec struct{} -// NewCodec returns an ASCII85 serializer. -func NewCodec() *Codec { return &Codec{} } + encoding "github.com/foomo/goencode" +) -func (Codec) Encode(v []byte) ([]byte, error) { +// Encoder encodes bytes to ASCII85. +func Encoder(v []byte) ([]byte, error) { dst := make([]byte, stdascii85.MaxEncodedLen(len(v))) n := stdascii85.Encode(dst, v) return dst[:n], nil } -func (Codec) Decode(b []byte, v *[]byte) error { +// Decoder decodes ASCII85 bytes. +func Decoder(b []byte, v *[]byte) error { buf := bytes.NewBuffer(make([]byte, 0, len(b))) - r := stdascii85.NewDecoder(bytes.NewReader(b)) + r := stdascii85.NewDecoder(bytes.NewReader(b)) if _, err := buf.ReadFrom(r); err != nil { return err } @@ -31,3 +28,12 @@ func (Codec) Decode(b []byte, v *[]byte) error { return nil } + +// NewCodec returns an ASCII85 codec. +// It is safe for concurrent use. +func NewCodec() encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: Encoder, + Decode: Decoder, + } +} diff --git a/ascii85/streamcodec.go b/ascii85/streamcodec.go index 26f9aae..9f57407 100644 --- a/ascii85/streamcodec.go +++ b/ascii85/streamcodec.go @@ -3,25 +3,21 @@ package ascii85 import ( stdascii85 "encoding/ascii85" "io" -) - -// StreamCodec is a StreamCodec[[]byte] backed by encoding/ascii85. -// It is safe for concurrent use. -type StreamCodec struct{} -// NewStreamCodec returns an ASCII85 stream serializer. -func NewStreamCodec() StreamCodec { return StreamCodec{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec) Encode(w io.Writer, v []byte) error { +// StreamEncoder encodes bytes to an ASCII85 stream. +func StreamEncoder(w io.Writer, v []byte) error { dst := make([]byte, stdascii85.MaxEncodedLen(len(v))) n := stdascii85.Encode(dst, v) - _, err := w.Write(dst[:n]) return err } -func (StreamCodec) Decode(r io.Reader, v *[]byte) error { +// StreamDecoder decodes bytes from an ASCII85 stream. +func StreamDecoder(r io.Reader, v *[]byte) error { data, err := io.ReadAll(stdascii85.NewDecoder(r)) if err != nil { return err @@ -31,3 +27,12 @@ func (StreamCodec) Decode(r io.Reader, v *[]byte) error { return nil } + +// NewStreamCodec returns an ASCII85 stream codec. +// It is safe for concurrent use. +func NewStreamCodec() encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: StreamEncoder, + Decode: StreamDecoder, + } +} diff --git a/asn1/codec.go b/asn1/codec.go index 1a543ca..447bf8c 100644 --- a/asn1/codec.go +++ b/asn1/codec.go @@ -2,21 +2,26 @@ package asn1 import ( stdasn1 "encoding/asn1" -) - -// Codec is a Codec[T] backed by encoding/asn1. -// It is safe for concurrent use. -type Codec[T any] struct{} -// NewCodec returns an ASN1 serializer for T. -func NewCodec[T any]() *Codec[T] { return &Codec[T]{} } + encoding "github.com/foomo/goencode" +) -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to ASN.1 bytes. +func Encoder[T any](v T) ([]byte, error) { return stdasn1.Marshal(v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes ASN.1 bytes into T. +func Decoder[T any](b []byte, v *T) error { _, err := stdasn1.Unmarshal(b, v) - return err } + +// NewCodec returns an ASN1 codec for T. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/asn1/codec_test.go b/asn1/codec_test.go index 912bff1..b720c50 100644 --- a/asn1/codec_test.go +++ b/asn1/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/asn1" ) -func ExampleCodec() { +func ExampleNewCodec() { c := asn1.NewCodec[int]() encoded, err := c.Encode(42) diff --git a/asn1/streamcodec.go b/asn1/streamcodec.go index 8c5573a..0db0095 100644 --- a/asn1/streamcodec.go +++ b/asn1/streamcodec.go @@ -3,16 +3,12 @@ package asn1 import ( stdasn1 "encoding/asn1" "io" -) - -// StreamCodec is a StreamCodec[T] backed by encoding/asn1. -// It is safe for concurrent use. -type StreamCodec[T any] struct{} -// NewStreamCodec returns an ASN1 stream serializer for T. -func NewStreamCodec[T any]() *StreamCodec[T] { return &StreamCodec[T]{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec[T]) Encode(w io.Writer, v T) error { +// StreamEncoder encodes T to an ASN.1 stream. +func StreamEncoder[T any](w io.Writer, v T) error { data, err := stdasn1.Marshal(v) if err != nil { return err @@ -23,7 +19,8 @@ func (StreamCodec[T]) Encode(w io.Writer, v T) error { return err } -func (StreamCodec[T]) Decode(r io.Reader, v *T) error { +// StreamDecoder decodes T from an ASN.1 stream. +func StreamDecoder[T any](r io.Reader, v *T) error { data, err := io.ReadAll(r) if err != nil { return err @@ -33,3 +30,12 @@ func (StreamCodec[T]) Decode(r io.Reader, v *T) error { return err } + +// NewStreamCodec returns an ASN.1 stream codec for T. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/asn1/streamcodec_test.go b/asn1/streamcodec_test.go index 9ae5760..32d0075 100644 --- a/asn1/streamcodec_test.go +++ b/asn1/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/asn1" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { c := asn1.NewStreamCodec[int]() var buf bytes.Buffer diff --git a/base32/codec.go b/base32/codec.go index 26d6796..d797ee3 100644 --- a/base32/codec.go +++ b/base32/codec.go @@ -2,23 +2,20 @@ package base32 import ( stdbase32 "encoding/base32" -) - -// Codec is a Codec[[]byte] backed by encoding/base32. -// It is safe for concurrent use. -type Codec struct{} -// NewCodec returns a Base32 serializer. -func NewCodec() *Codec { return &Codec{} } + encoding "github.com/foomo/goencode" +) -func (Codec) Encode(v []byte) ([]byte, error) { +// Encoder encodes bytes to Base32. +func Encoder(v []byte) ([]byte, error) { dst := make([]byte, stdbase32.StdEncoding.EncodedLen(len(v))) stdbase32.StdEncoding.Encode(dst, v) return dst, nil } -func (Codec) Decode(b []byte, v *[]byte) error { +// Decoder decodes Base32 bytes. +func Decoder(b []byte, v *[]byte) error { dst := make([]byte, stdbase32.StdEncoding.DecodedLen(len(b))) n, err := stdbase32.StdEncoding.Decode(dst, b) @@ -30,3 +27,12 @@ func (Codec) Decode(b []byte, v *[]byte) error { return nil } + +// NewCodec returns a Base32 codec. +// It is safe for concurrent use. +func NewCodec() encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: Encoder, + Decode: Decoder, + } +} diff --git a/base32/codec_test.go b/base32/codec_test.go index f0173a0..90a5720 100644 --- a/base32/codec_test.go +++ b/base32/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/base32" ) -func ExampleCodec() { +func ExampleNewCodec() { c := base32.NewCodec() encoded, err := c.Encode([]byte("hello")) diff --git a/base32/streamcodec.go b/base32/streamcodec.go index 9712b34..59f4cf3 100644 --- a/base32/streamcodec.go +++ b/base32/streamcodec.go @@ -3,16 +3,12 @@ package base32 import ( stdbase32 "encoding/base32" "io" -) - -// StreamCodec is a StreamCodec[[]byte] backed by encoding/base32. -// It is safe for concurrent use. -type StreamCodec struct{} -// NewStreamCodec returns a Base32 stream serializer. -func NewStreamCodec() *StreamCodec { return &StreamCodec{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec) Encode(w io.Writer, v []byte) error { +// StreamEncoder encodes bytes to a Base32 stream. +func StreamEncoder(w io.Writer, v []byte) error { enc := stdbase32.NewEncoder(stdbase32.StdEncoding, w) if _, err := enc.Write(v); err != nil { return err @@ -21,7 +17,8 @@ func (StreamCodec) Encode(w io.Writer, v []byte) error { return enc.Close() } -func (StreamCodec) Decode(r io.Reader, v *[]byte) error { +// StreamDecoder decodes bytes from a Base32 stream. +func StreamDecoder(r io.Reader, v *[]byte) error { data, err := io.ReadAll(stdbase32.NewDecoder(stdbase32.StdEncoding, r)) if err != nil { return err @@ -31,3 +28,12 @@ func (StreamCodec) Decode(r io.Reader, v *[]byte) error { return nil } + +// NewStreamCodec returns a Base32 stream codec. +// It is safe for concurrent use. +func NewStreamCodec() encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: StreamEncoder, + Decode: StreamDecoder, + } +} diff --git a/base32/streamcodec_test.go b/base32/streamcodec_test.go index 7327b31..0a7dc98 100644 --- a/base32/streamcodec_test.go +++ b/base32/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/base32" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { c := base32.NewStreamCodec() var buf bytes.Buffer diff --git a/base64/codec.go b/base64/codec.go index aad0fca..289f230 100644 --- a/base64/codec.go +++ b/base64/codec.go @@ -2,23 +2,20 @@ package base64 import ( stdbase64 "encoding/base64" -) - -// Codec is a Codec[[]byte] backed by encoding/base64. -// It is safe for concurrent use. -type Codec struct{} -// NewCodec returns a Base64 serializer. -func NewCodec() *Codec { return &Codec{} } + encoding "github.com/foomo/goencode" +) -func (Codec) Encode(v []byte) ([]byte, error) { +// Encoder encodes bytes to Base64. +func Encoder(v []byte) ([]byte, error) { dst := make([]byte, stdbase64.StdEncoding.EncodedLen(len(v))) stdbase64.StdEncoding.Encode(dst, v) return dst, nil } -func (Codec) Decode(b []byte, v *[]byte) error { +// Decoder decodes Base64 bytes. +func Decoder(b []byte, v *[]byte) error { dst := make([]byte, stdbase64.StdEncoding.DecodedLen(len(b))) n, err := stdbase64.StdEncoding.Decode(dst, b) @@ -30,3 +27,12 @@ func (Codec) Decode(b []byte, v *[]byte) error { return nil } + +// NewCodec returns a Base64 codec. +// It is safe for concurrent use. +func NewCodec() encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: Encoder, + Decode: Decoder, + } +} diff --git a/base64/codec_test.go b/base64/codec_test.go index ed0efb1..88fde5f 100644 --- a/base64/codec_test.go +++ b/base64/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/base64" ) -func ExampleCodec() { +func ExampleNewCodec() { c := base64.NewCodec() encoded, err := c.Encode([]byte("hello")) diff --git a/base64/streamcodec.go b/base64/streamcodec.go index effb0d7..c804e8c 100644 --- a/base64/streamcodec.go +++ b/base64/streamcodec.go @@ -3,16 +3,12 @@ package base64 import ( stdbase64 "encoding/base64" "io" -) - -// StreamCodec is a StreamCodec[[]byte] backed by encoding/base64. -// It is safe for concurrent use. -type StreamCodec struct{} -// NewStreamCodec returns a Base64 stream serializer. -func NewStreamCodec() StreamCodec { return StreamCodec{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec) Encode(w io.Writer, v []byte) error { +// StreamEncoder encodes bytes to a Base64 stream. +func StreamEncoder(w io.Writer, v []byte) error { enc := stdbase64.NewEncoder(stdbase64.StdEncoding, w) if _, err := enc.Write(v); err != nil { _ = enc.Close() @@ -22,7 +18,8 @@ func (StreamCodec) Encode(w io.Writer, v []byte) error { return enc.Close() } -func (StreamCodec) Decode(r io.Reader, v *[]byte) error { +// StreamDecoder decodes bytes from a Base64 stream. +func StreamDecoder(r io.Reader, v *[]byte) error { data, err := io.ReadAll(stdbase64.NewDecoder(stdbase64.StdEncoding, r)) if err != nil { return err @@ -32,3 +29,12 @@ func (StreamCodec) Decode(r io.Reader, v *[]byte) error { return nil } + +// NewStreamCodec returns a Base64 stream codec. +// It is safe for concurrent use. +func NewStreamCodec() encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: StreamEncoder, + Decode: StreamDecoder, + } +} diff --git a/base64/streamcodec_test.go b/base64/streamcodec_test.go index 4c00575..d56642b 100644 --- a/base64/streamcodec_test.go +++ b/base64/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/base64" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { c := base64.NewStreamCodec() var buf bytes.Buffer diff --git a/brotli/benchmark_test.go b/brotli/benchmark_test.go index 4573456..26f23d7 100644 --- a/brotli/benchmark_test.go +++ b/brotli/benchmark_test.go @@ -3,13 +3,14 @@ package brotli_test import ( "testing" + goencode "github.com/foomo/goencode" "github.com/foomo/goencode/brotli" "github.com/foomo/goencode/internal/testdata" - "github.com/foomo/goencode/json/v1" + json "github.com/foomo/goencode/json/v1" ) func BenchmarkCodec(b *testing.B) { - c := brotli.NewCodec(json.NewCodec[*testdata.User]()) + c := goencode.PipeCodec(json.NewCodec[*testdata.User](), brotli.NewCodec()) b.Run("encode", func(b *testing.B) { v := testdata.NewUser() diff --git a/brotli/codec.go b/brotli/codec.go index 97eb3b0..d332713 100644 --- a/brotli/codec.go +++ b/brotli/codec.go @@ -10,16 +10,8 @@ import ( "github.com/foomo/goencode/internal/sync" ) -// Codec is a Codec[T] that applies Brotli compression on top of another Codec[T]. -// It is safe for concurrent use. -type Codec[T any] struct { - codec encoding.Codec[T] - level int - maxDecodedSize int64 -} - -// NewCodec returns a Brotli compression codec that delegates serialization to codec. -func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { +// NewEncoder returns a Brotli compression encoder. +func NewEncoder(opts ...Option) encoding.Encoder[[]byte, []byte] { o := options{ level: brotli.DefaultCompression, } @@ -27,51 +19,59 @@ func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { opt(&o) } - return &Codec[T]{ - codec: codec, - level: o.level, - maxDecodedSize: o.maxDecodedSize, - } -} + return func(data []byte) ([]byte, error) { + buf := sync.Get() + defer sync.Put(buf) -func (c *Codec[T]) Encode(v T) ([]byte, error) { - b, err := c.codec.Encode(v) - if err != nil { - return nil, err - } + w := brotli.NewWriterLevel(buf, o.level) - buf := sync.Get() - defer sync.Put(buf) + if _, err := w.Write(data); err != nil { + return nil, err + } - w := brotli.NewWriterLevel(buf, c.level) + if err := w.Close(); err != nil { + return nil, err + } - if _, err := w.Write(b); err != nil { - return nil, err + return append([]byte(nil), buf.Bytes()...), nil } +} - if err := w.Close(); err != nil { - return nil, err +// NewDecoder returns a Brotli decompression decoder. +func NewDecoder(opts ...Option) encoding.Decoder[[]byte, []byte] { + o := options{} + for _, opt := range opts { + opt(&o) } - return append([]byte(nil), buf.Bytes()...), nil -} + return func(data []byte, v *[]byte) error { + r := brotli.NewReader(bytes.NewReader(data)) -func (c *Codec[T]) Decode(b []byte, v *T) error { - r := brotli.NewReader(bytes.NewReader(b)) + var src io.Reader = r + if o.maxDecodedSize > 0 { + src = io.LimitReader(r, o.maxDecodedSize+1) + } - var src io.Reader = r - if c.maxDecodedSize > 0 { - src = io.LimitReader(r, c.maxDecodedSize+1) - } + decoded, err := io.ReadAll(src) + if err != nil { + return err + } - data, err := io.ReadAll(src) - if err != nil { - return err - } + if o.maxDecodedSize > 0 && int64(len(decoded)) > o.maxDecodedSize { + return fmt.Errorf("brotli: decompressed size exceeds limit of %d bytes", o.maxDecodedSize) + } - if c.maxDecodedSize > 0 && int64(len(data)) > c.maxDecodedSize { - return fmt.Errorf("brotli: decompressed size exceeds limit of %d bytes", c.maxDecodedSize) + *v = decoded + + return nil } +} - return c.codec.Decode(data, v) +// NewCodec returns a Brotli compression codec. +// It is safe for concurrent use. +func NewCodec(opts ...Option) encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: NewEncoder(opts...), + Decode: NewDecoder(opts...), + } } diff --git a/brotli/codec_test.go b/brotli/codec_test.go index dc16bfa..494f801 100644 --- a/brotli/codec_test.go +++ b/brotli/codec_test.go @@ -3,16 +3,17 @@ package brotli_test import ( "fmt" + goencode "github.com/foomo/goencode" "github.com/foomo/goencode/brotli" - "github.com/foomo/goencode/json/v1" + json "github.com/foomo/goencode/json/v1" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } - c := brotli.NewCodec(json.NewCodec[Data]()) + c := goencode.PipeCodec(json.NewCodec[Data](), brotli.NewCodec()) encoded, err := c.Encode(Data{Name: "example-123"}) if err != nil { diff --git a/brotli/streamcodec.go b/brotli/streamcodec.go index 5836d2c..795a7f7 100644 --- a/brotli/streamcodec.go +++ b/brotli/streamcodec.go @@ -1,22 +1,15 @@ package brotli import ( + "fmt" "io" "github.com/andybalholm/brotli" encoding "github.com/foomo/goencode" ) -// StreamCodec is a StreamCodec[T] that applies Brotli compression on top of another StreamCodec[T]. -// It is safe for concurrent use. -type StreamCodec[T any] struct { - codec encoding.StreamCodec[T] - level int - maxDecodedSize int64 -} - -// NewStreamCodec returns a Brotli compression stream codec that delegates serialization to codec. -func NewStreamCodec[T any](codec encoding.StreamCodec[T], opts ...Option) *StreamCodec[T] { +// NewStreamEncoder returns a Brotli compression stream encoder. +func NewStreamEncoder(opts ...Option) encoding.StreamEncoder[[]byte] { o := options{ level: brotli.DefaultCompression, } @@ -24,31 +17,53 @@ func NewStreamCodec[T any](codec encoding.StreamCodec[T], opts ...Option) *Strea opt(&o) } - return &StreamCodec[T]{ - codec: codec, - level: o.level, - maxDecodedSize: o.maxDecodedSize, + return func(w io.Writer, data []byte) error { + bw := brotli.NewWriterLevel(w, o.level) + + if _, err := bw.Write(data); err != nil { + bw.Close() + return err + } + + return bw.Close() } } -func (c *StreamCodec[T]) Encode(w io.Writer, v T) error { - bw := brotli.NewWriterLevel(w, c.level) - - if err := c.codec.Encode(bw, v); err != nil { - bw.Close() - return err +// NewStreamDecoder returns a Brotli decompression stream decoder. +func NewStreamDecoder(opts ...Option) encoding.StreamDecoder[[]byte] { + o := options{} + for _, opt := range opts { + opt(&o) } - return bw.Close() -} + return func(r io.Reader, v *[]byte) error { + br := brotli.NewReader(r) + + var src io.Reader = br + if o.maxDecodedSize > 0 { + src = io.LimitReader(br, o.maxDecodedSize+1) + } + + data, err := io.ReadAll(src) + if err != nil { + return err + } + + if o.maxDecodedSize > 0 && int64(len(data)) > o.maxDecodedSize { + return fmt.Errorf("brotli: decompressed size exceeds limit of %d bytes", o.maxDecodedSize) + } -func (c *StreamCodec[T]) Decode(r io.Reader, v *T) error { - br := brotli.NewReader(r) + *v = data - var src io.Reader = br - if c.maxDecodedSize > 0 { - src = io.LimitReader(br, c.maxDecodedSize+1) + return nil } +} - return c.codec.Decode(src, v) +// NewStreamCodec returns a Brotli compression stream codec. +// It is safe for concurrent use. +func NewStreamCodec(opts ...Option) encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: NewStreamEncoder(opts...), + Decode: NewStreamDecoder(opts...), + } } diff --git a/brotli/streamcodec_test.go b/brotli/streamcodec_test.go index 5d7e5b9..16a736a 100644 --- a/brotli/streamcodec_test.go +++ b/brotli/streamcodec_test.go @@ -5,29 +5,26 @@ import ( "fmt" "github.com/foomo/goencode/brotli" - "github.com/foomo/goencode/json/v1" ) -func ExampleStreamCodec() { - type Data struct { - Name string - } +func ExampleNewStreamCodec() { + c := brotli.NewStreamCodec() - c := brotli.NewStreamCodec(json.NewStreamCodec[Data]()) + input := []byte("hello brotli stream") var buf bytes.Buffer - if err := c.Encode(&buf, Data{Name: "example-123"}); err != nil { + if err := c.Encode(&buf, input); err != nil { fmt.Printf("Encode failed: %v\n", err) return } - var decoded Data + var decoded []byte if err := c.Decode(&buf, &decoded); err != nil { fmt.Printf("Decode failed: %v\n", err) return } - fmt.Printf("Decoded Name: %s\n", decoded.Name) + fmt.Printf("Decoded: %s\n", string(decoded)) // Output: - // Decoded Name: example-123 + // Decoded: hello brotli stream } diff --git a/codec.go b/codec.go index 1ebf401..b34271b 100644 --- a/codec.go +++ b/codec.go @@ -1,9 +1,7 @@ package goencode -// Codec encodes T to []byte and decodes []byte back to T. -type Codec[T any] interface { - // Encode encodes v into bytes. - Encode(v T) ([]byte, error) - // Decode decodes b into v. - Decode(b []byte, v *T) error +// Codec bundles an Encoder and Decoder for S ↔ T round-trips. +type Codec[S, T any] struct { + Encode Encoder[S, T] + Decode Decoder[S, T] } diff --git a/csv/codec.go b/csv/codec.go index ac1bbfd..de3fd50 100644 --- a/csv/codec.go +++ b/csv/codec.go @@ -4,17 +4,12 @@ import ( "bytes" stdcsv "encoding/csv" + encoding "github.com/foomo/goencode" "github.com/foomo/goencode/internal/sync" ) -// Codec is a Codec[[][]string] backed by encoding/csv. -// It is safe for concurrent use. -type Codec struct{} - -// NewCodec returns a CSV serializer. -func NewCodec() *Codec { return &Codec{} } - -func (Codec) Encode(v [][]string) ([]byte, error) { +// Encoder encodes [][]string to CSV bytes. +func Encoder(v [][]string) ([]byte, error) { buf := sync.Get() defer sync.Put(buf) @@ -32,7 +27,8 @@ func (Codec) Encode(v [][]string) ([]byte, error) { return append([]byte(nil), buf.Bytes()...), nil } -func (Codec) Decode(b []byte, v *[][]string) error { +// Decoder decodes CSV bytes into [][]string. +func Decoder(b []byte, v *[][]string) error { records, err := stdcsv.NewReader(bytes.NewReader(b)).ReadAll() if err != nil { return err @@ -42,3 +38,12 @@ func (Codec) Decode(b []byte, v *[][]string) error { return nil } + +// NewCodec returns a CSV codec for [][]string. +// It is safe for concurrent use. +func NewCodec() encoding.Codec[[][]string, []byte] { + return encoding.Codec[[][]string, []byte]{ + Encode: Encoder, + Decode: Decoder, + } +} diff --git a/csv/codec_test.go b/csv/codec_test.go index 8bdd541..103bf00 100644 --- a/csv/codec_test.go +++ b/csv/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/csv" ) -func ExampleCodec() { +func ExampleNewCodec() { c := csv.NewCodec() records := [][]string{ diff --git a/csv/streamcodec.go b/csv/streamcodec.go index 91c0255..900551b 100644 --- a/csv/streamcodec.go +++ b/csv/streamcodec.go @@ -3,16 +3,12 @@ package csv import ( stdcsv "encoding/csv" "io" -) - -// StreamCodec is a StreamCodec[[][]string] backed by encoding/csv. -// It is safe for concurrent use. -type StreamCodec struct{} -// NewStreamCodec returns a CSV stream serializer. -func NewStreamCodec() *StreamCodec { return &StreamCodec{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec) Encode(w io.Writer, v [][]string) error { +// StreamEncoder encodes [][]string to a CSV stream. +func StreamEncoder(w io.Writer, v [][]string) error { cw := stdcsv.NewWriter(w) if err := cw.WriteAll(v); err != nil { return err @@ -23,7 +19,8 @@ func (StreamCodec) Encode(w io.Writer, v [][]string) error { return cw.Error() } -func (StreamCodec) Decode(r io.Reader, v *[][]string) error { +// StreamDecoder decodes [][]string from a CSV stream. +func StreamDecoder(r io.Reader, v *[][]string) error { records, err := stdcsv.NewReader(r).ReadAll() if err != nil { return err @@ -33,3 +30,12 @@ func (StreamCodec) Decode(r io.Reader, v *[][]string) error { return nil } + +// NewStreamCodec returns a CSV stream codec for [][]string. +// It is safe for concurrent use. +func NewStreamCodec() encoding.StreamCodec[[][]string] { + return encoding.StreamCodec[[][]string]{ + Encode: StreamEncoder, + Decode: StreamDecoder, + } +} diff --git a/csv/streamcodec_test.go b/csv/streamcodec_test.go index ea377fc..73dcc1e 100644 --- a/csv/streamcodec_test.go +++ b/csv/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/csv" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { c := csv.NewStreamCodec() records := [][]string{ diff --git a/decoder.go b/decoder.go index 33258d5..b8e38a3 100644 --- a/decoder.go +++ b/decoder.go @@ -1,5 +1,4 @@ package goencode -type Decoder[T any] interface { - Decode(v any) error -} +// Decoder decodes target T back into source S. +type Decoder[S, T any] func(t T, s *S) error diff --git a/decoderfunc.go b/decoderfunc.go deleted file mode 100644 index f5277dc..0000000 --- a/decoderfunc.go +++ /dev/null @@ -1,7 +0,0 @@ -package goencode - -type DecoderFunc[T any] func(v any) error - -func (f DecoderFunc[T]) Decode(v any) error { - return f(v) -} diff --git a/docs/guide/codecs.md b/docs/guide/codecs.md index 6a99027..6c8c3f0 100644 --- a/docs/guide/codecs.md +++ b/docs/guide/codecs.md @@ -1,6 +1,6 @@ # Codec Reference -All codecs listed below implement `Codec[T]`, `StreamCodec[T]`, or both. The core module packages use only the Go standard library. +All codecs listed below return `Codec[S, T]` and/or `StreamCodec[S]` structs via constructor functions. Each package also exports standalone `Encoder` and `Decoder` functions. The core module packages use only the Go standard library. ## Serialization Codecs @@ -22,19 +22,22 @@ All codecs listed below implement `Codec[T]`, `StreamCodec[T]`, or both. The cor | `hex` | `hex.NewCodec()` | `[]byte` | `hex.NewStreamCodec()` | | `ascii85` | `ascii85.NewCodec()` | `[]byte` | `ascii85.NewStreamCodec()` | -## Compression Wrappers +## Compression Codecs -Compression codecs wrap an inner `Codec[T]` or `StreamCodec[T]` using the [decorator pattern](/guide/composition). +Compression codecs are standalone `Codec[[]byte, []byte]` — they compress and decompress raw bytes. Compose them with serialization codecs via [`PipeCodec`](/guide/composition). -| Package | Constructor | Options | -|---------|-------------|---------| -| `gzip` | `gzip.NewCodec[T](codec, opts...)` | `gzip.WithLevel(int)` | -| `flate` | `flate.NewCodec[T](codec, opts...)` | `flate.WithLevel(int)` | -| `snappy` | `snappy.NewCodec[T](codec)` | — | -| `zstd` | `zstd.NewCodec[T](codec, opts...)` | `zstd.WithLevel(zstd.EncoderLevel)` | -| `brotli` | `brotli.NewCodec[T](codec, opts...)` | `brotli.WithLevel(int)` | +| Package | Constructor | StreamCodec | Options | +|---------|-------------|-------------|---------| +| `gzip` | `gzip.NewCodec(opts...)` | `gzip.NewStreamCodec(opts...)` | `gzip.WithLevel(int)` | +| `flate` | `flate.NewCodec(opts...)` | `flate.NewStreamCodec(opts...)` | `flate.WithLevel(int)` | +| `snappy` | `snappy.NewCodec()` | `snappy.NewStreamCodec()` | — | +| `zstd` | `zstd.NewCodec(opts...)` | `zstd.NewStreamCodec(opts...)` | `zstd.WithLevel(zstd.EncoderLevel)` | +| `brotli` | `brotli.NewCodec(opts...)` | `brotli.NewStreamCodec(opts...)` | `brotli.WithLevel(int)` | -Each also has a stream variant: `gzip.NewStreamCodec[T](streamCodec, opts...)`, etc. +```go +// Example: JSON + gzip via PipeCodec +c := goencode.PipeCodec(json.NewCodec[User](), gzip.NewCodec()) +``` ::: tip `snappy`, `zstd`, and `brotli` are [submodule packages](#submodule-packages) that require a separate `go get`. @@ -48,14 +51,14 @@ Each also has a stream variant: `gzip.NewStreamCodec[T](streamCodec, opts...)`, The `file` package wraps any codec to read/write files atomically (temp file + rename). ```go -file.NewCodec[T](codec, opts...) // Encode(path string, v T) error -file.NewStreamCodec[T](codec, opts...) // Encode(path string, v T) error +file.NewCodec[T](codec, opts...) // accepts Codec[T, []byte], Encode(path string, v T) error +file.NewStreamCodec[T](codec, opts...) // accepts StreamCodec[T], Encode(path string, v T) error ``` Options: `file.WithPermissions(os.FileMode)` — default `0o644`. ::: warning -The file codec has a different method signature — it uses `path string` instead of `[]byte` or `io.Writer`. It does not satisfy the `Codec[T]` or `StreamCodec[T]` interfaces. See [File Codec](/guide/file-codec) for details. +The file codec has a different method signature — it uses `path string` instead of `[]byte` or `io.Writer`. See [File Codec](/guide/file-codec) for details. ::: ## Submodule Packages @@ -64,11 +67,13 @@ These packages have external dependencies and live in separate Go modules. Insta | Package | Import Path | Dependency | StreamCodec | |---------|-------------|------------|-------------| -| `json2` | `github.com/foomo/goencode/json2` | go-json-experiment | — (has `EncodeTo`/`DecodeFrom` methods) | -| `yaml/v2` | `github.com/foomo/goencode/yaml/v2` | go.yaml.in/yaml/v2 | — | -| `yaml/v3` | `github.com/foomo/goencode/yaml/v3` | go.yaml.in/yaml/v3 | — | -| `yaml/v4` | `github.com/foomo/goencode/yaml/v4` | go.yaml.in/yaml/v4 | — | -| `snappy` | `github.com/foomo/goencode/snappy` | github.com/golang/snappy | `snappy.NewStreamCodec[T](codec)` | -| `brotli` | `github.com/foomo/goencode/brotli` | github.com/andybalholm/brotli | `brotli.NewStreamCodec[T](codec, opts...)` | +| `json/v2` | `github.com/foomo/goencode/json/v2` | go-json-experiment | `json.NewStreamCodec[T]()` | +| `yaml/v2` | `github.com/foomo/goencode/yaml/v2` | go.yaml.in/yaml/v2 | `yaml.NewStreamCodec[T]()` | +| `yaml/v3` | `github.com/foomo/goencode/yaml/v3` | go.yaml.in/yaml/v3 | `yaml.NewStreamCodec[T]()` | +| `yaml/v4` | `github.com/foomo/goencode/yaml/v4` | go.yaml.in/yaml/v4 | `yaml.NewStreamCodec[T]()` | | `toml` | `github.com/foomo/goencode/toml` | github.com/BurntSushi/toml | `toml.NewStreamCodec[T]()` | -| `zstd` | `github.com/foomo/goencode/zstd` | github.com/klauspost/compress | `zstd.NewStreamCodec[T](codec, opts...)` | +| `msgpack/tinylib` | `github.com/foomo/goencode/msgpack/tinylib` | github.com/tinylib/msgp | `msgpack.NewStreamCodec[T]()` | +| `msgpack/vmihailenco` | `github.com/foomo/goencode/msgpack/vmihailenco` | github.com/vmihailenco/msgpack | `msgpack.NewStreamCodec[T]()` | +| `snappy` | `github.com/foomo/goencode/snappy` | github.com/golang/snappy | `snappy.NewStreamCodec()` | +| `brotli` | `github.com/foomo/goencode/brotli` | github.com/andybalholm/brotli | `brotli.NewStreamCodec(opts...)` | +| `zstd` | `github.com/foomo/goencode/zstd` | github.com/klauspost/compress | `zstd.NewStreamCodec(opts...)` | diff --git a/docs/guide/composition.md b/docs/guide/composition.md index 680f334..0b3de5c 100644 --- a/docs/guide/composition.md +++ b/docs/guide/composition.md @@ -1,13 +1,14 @@ # Composing Codecs -Compression codecs in goencode follow the decorator pattern — they wrap an inner `Codec[T]` to add a compression layer. This lets you compose serialization and compression in a single line. +Codecs in goencode compose via `PipeCodec` — a type-safe function that chains two codecs together. Compression codecs are standalone `Codec[[]byte, []byte]`, so you pipe a serialization codec into a compression codec to get a single composed codec. ## Basic Composition -A compression codec takes any `Codec[T]` as its first argument: +`PipeCodec` chains two codecs where the output type of the first matches the input type of the second: ```go import ( + "github.com/foomo/goencode" "github.com/foomo/goencode/gzip" "github.com/foomo/goencode/json/v1" ) @@ -18,7 +19,7 @@ type User struct { } // JSON serialization + gzip compression -c := gzip.NewCodec[User](json.NewCodec[User]()) // [!code highlight] +c := goencode.PipeCodec(json.NewCodec[User](), gzip.NewCodec()) // [!code highlight] b, err := c.Encode(User{Name: "Alice", Age: 30}) // b contains gzip-compressed JSON @@ -28,60 +29,73 @@ err = c.Decode(b, &u) // u == User{Name: "Alice", Age: 30} ``` -The flow is: `Encode` serializes with the inner codec, then compresses. `Decode` decompresses, then deserializes. +The flow is: `Encode` serializes with the first codec, then compresses with the second. `Decode` decompresses with the second, then deserializes with the first. ## Choosing a Format ::: code-group ```go [JSON + gzip] -c := gzip.NewCodec[User](json.NewCodec[User]()) +c := goencode.PipeCodec(json.NewCodec[User](), gzip.NewCodec()) ``` ```go [XML + flate] -c := flate.NewCodec[User](xml.NewCodec[User]()) +c := goencode.PipeCodec(xml.NewCodec[User](), flate.NewCodec()) ``` ```go [Gob + snappy] -c := snappy.NewCodec[User](gob.NewCodec[User]()) +c := goencode.PipeCodec(gob.NewCodec[User](), snappy.NewCodec()) ``` ```go [JSON + zstd] -c := zstd.NewCodec[User](json.NewCodec[User]()) +c := goencode.PipeCodec(json.NewCodec[User](), zstd.NewCodec()) ``` ::: ## Compression Options -gzip, flate, and zstd accept options to tune compression level: +gzip, flate, zstd, and brotli accept options to tune compression level: ```go // gzip with best compression -c := gzip.NewCodec[User]( +c := goencode.PipeCodec( json.NewCodec[User](), - gzip.WithLevel(gzip.BestCompression), // [!code highlight] + gzip.NewCodec(gzip.WithLevel(gzip.BestCompression)), // [!code highlight] ) // flate with best speed -c := flate.NewCodec[User]( +c := goencode.PipeCodec( json.NewCodec[User](), - flate.WithLevel(flate.BestSpeed), // [!code highlight] + flate.NewCodec(flate.WithLevel(flate.BestSpeed)), // [!code highlight] ) // zstd with best compression -c := zstd.NewCodec[User]( +c := goencode.PipeCodec( json.NewCodec[User](), - zstd.WithLevel(zstd.SpeedBestCompression), // [!code highlight] + zstd.NewCodec(zstd.WithLevel(zstd.SpeedBestCompression)), // [!code highlight] +) +``` + +## Chaining Multiple Codecs + +`PipeCodec` returns a `Codec`, so you can chain more than two: + +```go +// JSON → gzip → base64 +c := goencode.PipeCodec( + goencode.PipeCodec(json.NewCodec[User](), gzip.NewCodec()), + base64.NewCodec(), ) ``` ## Adding File Persistence -The `file` codec wraps any `Codec[T]` to read and write files atomically: +The `file` codec wraps any `Codec[T, []byte]` to read and write files atomically: ```go import ( + "github.com/foomo/goencode" "github.com/foomo/goencode/file" "github.com/foomo/goencode/gzip" "github.com/foomo/goencode/json/v1" @@ -94,7 +108,7 @@ type Config struct { // JSON + gzip + atomic file I/O fc := file.NewCodec[Config]( // [!code highlight] - gzip.NewCodec[Config](json.NewCodec[Config]()), // [!code highlight] + goencode.PipeCodec(json.NewCodec[Config](), gzip.NewCodec()), // [!code highlight] file.WithPermissions(0o600), ) // [!code highlight] @@ -106,24 +120,16 @@ var loaded Config err = fc.Decode("/etc/myapp/config.json.gz", &loaded) ``` -The composition layers from outside in: `file` → `gzip` → `json` → `T`. +The composition layers: `file` wraps the piped codec (`json` → `gzip`). -## Stream Composition +## Standalone Encoder/Decoder Composition -The same pattern works with `StreamCodec[T]`: +You can also compose individual encoder and decoder functions: ```go -import ( - "github.com/foomo/goencode/gzip" - "github.com/foomo/goencode/json/v1" -) +// Compose encoders: User → []byte → []byte +enc := goencode.PipeEncoder(json.Encoder[User], gzip.NewEncoder()) -sc := gzip.NewStreamCodec[User](json.NewStreamCodec[User]()) // [!code highlight] - -// Write compressed JSON to any io.Writer -err := sc.Encode(writer, user) - -// Read compressed JSON from any io.Reader -var u User -err = sc.Decode(reader, &u) +// Compose decoders: []byte → []byte → User +dec := goencode.PipeDecoder(json.Decoder[User], gzip.NewDecoder()) ``` diff --git a/docs/guide/file-codec.md b/docs/guide/file-codec.md index 3b04cff..430ca6b 100644 --- a/docs/guide/file-codec.md +++ b/docs/guide/file-codec.md @@ -1,9 +1,9 @@ # File Codec -The `file` package wraps any `Codec[T]` or `StreamCodec[T]` to add atomic file persistence. It writes to a temporary file first, then renames it into place — preventing partial writes if the process crashes mid-write. +The `file` package wraps any `Codec[T, []byte]` or `StreamCodec[T]` to add atomic file persistence. It writes to a temporary file first, then renames it into place — preventing partial writes if the process crashes mid-write. ::: warning -The file codec has a different method signature from `Codec[T]` — it uses `path string` instead of `[]byte`. It does not satisfy the `Codec[T]` or `StreamCodec[T]` interfaces. +The file codec has a different method signature — it uses `path string` instead of `[]byte` or `io.Writer`. ::: ## Basic Usage @@ -54,7 +54,7 @@ fc := file.NewCodec[Secrets]( ## Stream Variant -`file.NewStreamCodec[T]` wraps a `StreamCodec[T]` instead. This streams the encoded data directly to the temp file without buffering the full payload in memory. +`file.NewStreamCodec[T]` wraps a `StreamCodec[T]` struct instead. This streams the encoded data directly to the temp file without buffering the full payload in memory. ```go fsc := file.NewStreamCodec[Config](json.NewStreamCodec[Config]()) // [!code highlight] @@ -67,10 +67,11 @@ err = fsc.Decode("config.json", &loaded) ## Composed Example -Combine serialization, compression, and file persistence: +Combine serialization, compression, and file persistence via `PipeCodec`: ```go import ( + "github.com/foomo/goencode" "github.com/foomo/goencode/file" "github.com/foomo/goencode/gzip" "github.com/foomo/goencode/json/v1" @@ -82,10 +83,7 @@ type State struct { } fc := file.NewCodec[State]( - gzip.NewCodec[State]( - json.NewCodec[State](), - gzip.WithLevel(gzip.BestSpeed), - ), + goencode.PipeCodec(json.NewCodec[State](), gzip.NewCodec(gzip.WithLevel(gzip.BestSpeed))), file.WithPermissions(0o600), ) diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 902961f..6d8ddfc 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -13,41 +13,53 @@ These packages have external dependencies and require their own `go get`: | Package | Install | |---------|---------| -| `json2` | `go get github.com/foomo/goencode/json2` | +| `json/v2` | `go get github.com/foomo/goencode/json/v2` | | `yaml/v2` | `go get github.com/foomo/goencode/yaml/v2` | | `yaml/v3` | `go get github.com/foomo/goencode/yaml/v3` | | `yaml/v4` | `go get github.com/foomo/goencode/yaml/v4` | +| `toml` | `go get github.com/foomo/goencode/toml` | | `snappy` | `go get github.com/foomo/goencode/snappy` | | `zstd` | `go get github.com/foomo/goencode/zstd` | +| `brotli` | `go get github.com/foomo/goencode/brotli` | +| `msgpack/tinylib` | `go get github.com/foomo/goencode/msgpack/tinylib` | +| `msgpack/vmihailenco` | `go get github.com/foomo/goencode/msgpack/vmihailenco` | ::: -## Core Interfaces +## Core Types -goencode defines two generic interfaces at the root package. +goencode defines function types and struct bundles at the root package. -### Codec[T] — byte-oriented +### Codec[S, T] — byte-oriented ```go -// Codec encodes T to []byte and decodes []byte back to T. -type Codec[T any] interface { - Encode(v T) ([]byte, error) - Decode(b []byte, v *T) error +// Function types +type Encoder[S, T any] func(s S) (T, error) +type Decoder[S, T any] func(t T, s *S) error + +// Codec bundles an Encoder and Decoder pair. +type Codec[S, T any] struct { + Encode Encoder[S, T] + Decode Decoder[S, T] } ``` -Use `Codec[T]` when you need the encoded result as a byte slice — for example, storing in a database, sending over a message queue, or passing to another function. +Use `Codec[S, T]` when you need the encoded result as a value — for example, `Codec[User, []byte]` for serialization or `Codec[[]byte, []byte]` for compression. Codecs compose via `PipeCodec` for type-safe chaining. -### StreamCodec[T] — io.Reader/io.Writer-oriented +### StreamCodec[S] — io.Reader/io.Writer-oriented ```go -// StreamCodec encodes T to an io.Writer and decodes T from an io.Reader. -type StreamCodec[T any] interface { - Encode(w io.Writer, v T) error - Decode(r io.Reader, v *T) error +// Stream function types +type StreamEncoder[S any] func(w io.Writer, s S) error +type StreamDecoder[S any] func(r io.Reader, s *S) error + +// StreamCodec bundles a StreamEncoder and StreamDecoder pair. +type StreamCodec[S any] struct { + Encode StreamEncoder[S] + Decode StreamDecoder[S] } ``` -Use `StreamCodec[T]` when working with streams — HTTP request/response bodies, files, network connections, or any `io.Reader`/`io.Writer`. +Use `StreamCodec[S]` when working with streams — HTTP request/response bodies, files, network connections, or any `io.Reader`/`io.Writer`. ## Minimal Example @@ -87,4 +99,4 @@ func main() { ## Concurrency Safety -All codecs in this library are safe for concurrent use. Serialization codecs like `json.Codec[T]` are stateless zero-size structs; compression wrappers hold only immutable configuration. You can safely share a single codec instance across goroutines. +All codecs in this library are safe for concurrent use. Codec structs bundle pure function values with no shared mutable state. You can safely share a single codec instance across goroutines. diff --git a/docs/guide/streaming.md b/docs/guide/streaming.md index d169f0f..54beaaa 100644 --- a/docs/guide/streaming.md +++ b/docs/guide/streaming.md @@ -1,27 +1,22 @@ # Streaming -`StreamCodec[T]` encodes to an `io.Writer` and decodes from an `io.Reader`. Use it when working with streams — HTTP bodies, files, network connections, or pipelines — to avoid buffering entire payloads in memory. +`StreamCodec[S]` encodes to an `io.Writer` and decodes from an `io.Reader`. Use it when working with streams — HTTP bodies, files, network connections, or pipelines — to avoid buffering entire payloads in memory. -## Interface +## Types ```go -type StreamCodec[T any] interface { - Encode(w io.Writer, v T) error - Decode(r io.Reader, v *T) error +// Stream function types +type StreamEncoder[S any] func(w io.Writer, s S) error +type StreamDecoder[S any] func(r io.Reader, s *S) error + +// StreamCodec bundles a StreamEncoder and StreamDecoder pair. +type StreamCodec[S any] struct { + Encode StreamEncoder[S] + Decode StreamDecoder[S] } ``` -The root package also defines `Encoder[T]` and `Decoder[T]` for stateful encoder/decoder pairs: - -```go -type Encoder[T any] interface { - Encode(v T) error -} - -type Decoder[T any] interface { - Decode(v any) error -} -``` +Each serialization package exports standalone `StreamEncoder` and `StreamDecoder` functions alongside the `NewStreamCodec` constructor. ## JSON Streaming @@ -101,30 +96,29 @@ _ = c.Decode(&buf, &decoded) // [!code highlight] fmt.Println(decoded) // [[name age] [Alice 30]] ``` -## Compressed Streams +## Compression Streaming -Stream codecs compose the same way as byte codecs — compression wrappers accept an inner `StreamCodec[T]`: +Compression stream codecs are standalone `StreamCodec[[]byte]` — they compress and decompress raw bytes over streams: ```go import ( "github.com/foomo/goencode/gzip" - "github.com/foomo/goencode/json/v1" ) -type Payload struct { - Items []string `json:"items"` -} - -sc := gzip.NewStreamCodec[Payload](json.NewStreamCodec[Payload]()) // [!code highlight] +sc := gzip.NewStreamCodec() // StreamCodec[[]byte] // [!code highlight] -// Write gzip-compressed JSON to any io.Writer -err := sc.Encode(writer, payload) +// Write gzip-compressed bytes to any io.Writer +err := sc.Encode(writer, rawBytes) -// Read gzip-compressed JSON from any io.Reader -var p Payload -err = sc.Decode(reader, &p) +// Read gzip-decompressed bytes from any io.Reader +var decoded []byte +err = sc.Decode(reader, &decoded) ``` +::: tip +For combined serialization + compression (e.g., JSON → gzip), use the byte-oriented `PipeCodec` approach instead. See [Composing Codecs](/guide/composition) for details. +::: + ## HTTP Example StreamCodec is a natural fit for HTTP handlers: diff --git a/docs/index.md b/docs/index.md index b8a596d..2ce2897 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ layout: home hero: name: goencode text: Generic Encoding for Go - tagline: Composable, type-safe codec interfaces. Serialize, compress, and persist data with a single API. + tagline: Composable, type-safe codec function types. Serialize, compress, and persist data with a single API. image: src: /logo.png alt: goencode @@ -18,11 +18,11 @@ hero: features: - title: Type-Safe Generics - details: Codec[T] and StreamCodec[T] use Go generics so encode/decode operations are statically typed at compile time. - - title: Composable Wrappers - details: Layer gzip, flate, snappy, or zstd compression on any codec with a single function call using the decorator pattern. + details: Codec[S, T] and StreamCodec[S] use Go generics so encode/decode operations are statically typed at compile time. + - title: Composable Pipelines + details: Chain any two codecs with PipeCodec — e.g., JSON → gzip, JSON → base64 — with full type safety at compile time. - title: Streaming Support - details: StreamCodec[T] reads and writes directly to io.Reader/io.Writer for memory-efficient pipelines and network I/O. + details: StreamCodec[S] reads and writes directly to io.Reader/io.Writer for memory-efficient pipelines and network I/O. - title: Atomic File I/O details: The file codec writes to a temp file and renames into place, preventing partial writes and data corruption. - title: Zero Dependencies diff --git a/encoder.go b/encoder.go index d4ee713..ebf43c4 100644 --- a/encoder.go +++ b/encoder.go @@ -1,5 +1,4 @@ package goencode -type Encoder[T any] interface { - Encode(v T) error -} +// Encoder encodes source S to target T. +type Encoder[S, T any] func(s S) (T, error) diff --git a/encoderfunc.go b/encoderfunc.go deleted file mode 100644 index fc605dc..0000000 --- a/encoderfunc.go +++ /dev/null @@ -1,7 +0,0 @@ -package goencode - -type EncoderFunc[T any] func(v T) error - -func (f EncoderFunc[T]) Encode(v T) error { - return f(v) -} diff --git a/file/codec.go b/file/codec.go index 7eafe84..87a0c7e 100644 --- a/file/codec.go +++ b/file/codec.go @@ -8,16 +8,16 @@ import ( encoding "github.com/foomo/goencode" ) -// Codec encodes T to a file and decodes T from a file using an underlying Codec[T]. +// Codec encodes T to a file and decodes T from a file using an underlying Codec[T, []byte]. // Writes are atomic: data is written to a temporary file and renamed into place. // It is safe for concurrent use. type Codec[T any] struct { - codec encoding.Codec[T] + codec encoding.Codec[T, []byte] perm os.FileMode } // NewCodec returns a file codec that delegates serialization to codec. -func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { +func NewCodec[T any](codec encoding.Codec[T, []byte], opts ...Option) *Codec[T] { o := options{ perm: 0o644, } diff --git a/flate/benchmark_test.go b/flate/benchmark_test.go index 3f9cfbe..9b17413 100644 --- a/flate/benchmark_test.go +++ b/flate/benchmark_test.go @@ -3,13 +3,14 @@ package flate_test import ( "testing" + goencode "github.com/foomo/goencode" "github.com/foomo/goencode/flate" "github.com/foomo/goencode/internal/testdata" json "github.com/foomo/goencode/json/v1" ) func BenchmarkCodec(b *testing.B) { - c := flate.NewCodec(json.NewCodec[*testdata.User]()) + c := goencode.PipeCodec(json.NewCodec[*testdata.User](), flate.NewCodec()) b.Run("encode", func(b *testing.B) { v := testdata.NewUser() diff --git a/flate/codec.go b/flate/codec.go index 5c72bf6..d695840 100644 --- a/flate/codec.go +++ b/flate/codec.go @@ -10,16 +10,8 @@ import ( "github.com/foomo/goencode/internal/sync" ) -// Codec is a Codec[T] that applies DEFLATE compression on top of another Codec[T]. -// It is safe for concurrent use. -type Codec[T any] struct { - codec encoding.Codec[T] - level int - maxDecodedSize int64 -} - -// NewCodec returns a flate compression codec that delegates serialization to codec. -func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { +// NewEncoder returns a DEFLATE compression encoder. +func NewEncoder(opts ...Option) encoding.Encoder[[]byte, []byte] { o := options{ level: flate.DefaultCompression, } @@ -27,55 +19,63 @@ func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { opt(&o) } - return &Codec[T]{ - codec: codec, - level: o.level, - maxDecodedSize: o.maxDecodedSize, - } -} + return func(data []byte) ([]byte, error) { + buf := sync.Get() + defer sync.Put(buf) -func (c *Codec[T]) Encode(v T) ([]byte, error) { - b, err := c.codec.Encode(v) - if err != nil { - return nil, err - } + w, err := flate.NewWriter(buf, o.level) + if err != nil { + return nil, err + } - buf := sync.Get() - defer sync.Put(buf) + if _, err := w.Write(data); err != nil { + return nil, err + } - w, err := flate.NewWriter(buf, c.level) - if err != nil { - return nil, err - } + if err := w.Close(); err != nil { + return nil, err + } - if _, err := w.Write(b); err != nil { - return nil, err + return append([]byte(nil), buf.Bytes()...), nil } +} - if err := w.Close(); err != nil { - return nil, err +// NewDecoder returns a DEFLATE decompression decoder. +func NewDecoder(opts ...Option) encoding.Decoder[[]byte, []byte] { + o := options{} + for _, opt := range opts { + opt(&o) } - return append([]byte(nil), buf.Bytes()...), nil -} + return func(data []byte, v *[]byte) error { + r := flate.NewReader(bytes.NewReader(data)) + defer r.Close() -func (c *Codec[T]) Decode(b []byte, v *T) error { - r := flate.NewReader(bytes.NewReader(b)) - defer r.Close() + var src io.Reader = r + if o.maxDecodedSize > 0 { + src = io.LimitReader(r, o.maxDecodedSize+1) + } - var src io.Reader = r - if c.maxDecodedSize > 0 { - src = io.LimitReader(r, c.maxDecodedSize+1) - } + decoded, err := io.ReadAll(src) + if err != nil { + return err + } - data, err := io.ReadAll(src) - if err != nil { - return err - } + if o.maxDecodedSize > 0 && int64(len(decoded)) > o.maxDecodedSize { + return fmt.Errorf("flate: decompressed size exceeds limit of %d bytes", o.maxDecodedSize) + } - if c.maxDecodedSize > 0 && int64(len(data)) > c.maxDecodedSize { - return fmt.Errorf("flate: decompressed size exceeds limit of %d bytes", c.maxDecodedSize) + *v = decoded + + return nil } +} - return c.codec.Decode(data, v) +// NewCodec returns a DEFLATE compression codec. +// It is safe for concurrent use. +func NewCodec(opts ...Option) encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: NewEncoder(opts...), + Decode: NewDecoder(opts...), + } } diff --git a/flate/codec_test.go b/flate/codec_test.go index 27ea10f..3203595 100644 --- a/flate/codec_test.go +++ b/flate/codec_test.go @@ -3,16 +3,17 @@ package flate_test import ( "fmt" + goencode "github.com/foomo/goencode" "github.com/foomo/goencode/flate" - "github.com/foomo/goencode/json/v1" + json "github.com/foomo/goencode/json/v1" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } - c := flate.NewCodec(json.NewCodec[Data]()) + c := goencode.PipeCodec(json.NewCodec[Data](), flate.NewCodec()) encoded, err := c.Encode(Data{Name: "example-123"}) if err != nil { diff --git a/flate/streamcodec.go b/flate/streamcodec.go index 4451132..a23d6dc 100644 --- a/flate/streamcodec.go +++ b/flate/streamcodec.go @@ -2,21 +2,14 @@ package flate import ( "compress/flate" + "fmt" "io" encoding "github.com/foomo/goencode" ) -// StreamCodec is a StreamCodec[T] that applies DEFLATE compression on top of another StreamCodec[T]. -// It is safe for concurrent use. -type StreamCodec[T any] struct { - codec encoding.StreamCodec[T] - level int - maxDecodedSize int64 -} - -// NewStreamCodec returns a flate compression stream codec that delegates serialization to codec. -func NewStreamCodec[T any](codec encoding.StreamCodec[T], opts ...Option) *StreamCodec[T] { +// NewStreamEncoder returns a DEFLATE compression stream encoder. +func NewStreamEncoder(opts ...Option) encoding.StreamEncoder[[]byte] { o := options{ level: flate.DefaultCompression, } @@ -24,35 +17,57 @@ func NewStreamCodec[T any](codec encoding.StreamCodec[T], opts ...Option) *Strea opt(&o) } - return &StreamCodec[T]{ - codec: codec, - level: o.level, - maxDecodedSize: o.maxDecodedSize, + return func(w io.Writer, data []byte) error { + fw, err := flate.NewWriter(w, o.level) + if err != nil { + return err + } + + if _, err := fw.Write(data); err != nil { + fw.Close() + return err + } + + return fw.Close() } } -func (c *StreamCodec[T]) Encode(w io.Writer, v T) error { - fw, err := flate.NewWriter(w, c.level) - if err != nil { - return err +// NewStreamDecoder returns a DEFLATE decompression stream decoder. +func NewStreamDecoder(opts ...Option) encoding.StreamDecoder[[]byte] { + o := options{} + for _, opt := range opts { + opt(&o) } - if err := c.codec.Encode(fw, v); err != nil { - fw.Close() - return err - } + return func(r io.Reader, v *[]byte) error { + fr := flate.NewReader(r) + defer fr.Close() - return fw.Close() -} + var src io.Reader = fr + if o.maxDecodedSize > 0 { + src = io.LimitReader(fr, o.maxDecodedSize+1) + } + + data, err := io.ReadAll(src) + if err != nil { + return err + } + + if o.maxDecodedSize > 0 && int64(len(data)) > o.maxDecodedSize { + return fmt.Errorf("flate: decompressed size exceeds limit of %d bytes", o.maxDecodedSize) + } -func (c *StreamCodec[T]) Decode(r io.Reader, v *T) error { - fr := flate.NewReader(r) - defer fr.Close() + *v = data - var src io.Reader = fr - if c.maxDecodedSize > 0 { - src = io.LimitReader(fr, c.maxDecodedSize+1) + return nil } +} - return c.codec.Decode(src, v) +// NewStreamCodec returns a DEFLATE compression stream codec. +// It is safe for concurrent use. +func NewStreamCodec(opts ...Option) encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: NewStreamEncoder(opts...), + Decode: NewStreamDecoder(opts...), + } } diff --git a/flate/streamcodec_test.go b/flate/streamcodec_test.go index 38e544a..a7f3c17 100644 --- a/flate/streamcodec_test.go +++ b/flate/streamcodec_test.go @@ -5,29 +5,26 @@ import ( "fmt" "github.com/foomo/goencode/flate" - json "github.com/foomo/goencode/json/v1" ) -func ExampleStreamCodec() { - type Data struct { - Name string - } +func ExampleNewStreamCodec() { + c := flate.NewStreamCodec() - c := flate.NewStreamCodec(json.NewStreamCodec[Data]()) + input := []byte("hello flate stream") var buf bytes.Buffer - if err := c.Encode(&buf, Data{Name: "example-123"}); err != nil { + if err := c.Encode(&buf, input); err != nil { fmt.Printf("Encode failed: %v\n", err) return } - var decoded Data + var decoded []byte if err := c.Decode(&buf, &decoded); err != nil { fmt.Printf("Decode failed: %v\n", err) return } - fmt.Printf("Decoded Name: %s\n", decoded.Name) + fmt.Printf("Decoded: %s\n", string(decoded)) // Output: - // Decoded Name: example-123 + // Decoded: hello flate stream } diff --git a/gob/codec.go b/gob/codec.go index 3e28955..a890722 100644 --- a/gob/codec.go +++ b/gob/codec.go @@ -4,17 +4,12 @@ import ( "bytes" stdgob "encoding/gob" + encoding "github.com/foomo/goencode" "github.com/foomo/goencode/internal/sync" ) -// Codec is a Codec[T] backed by encoding/gob. -// It is safe for concurrent use. -type Codec[T any] struct{} - -// NewCodec returns a GOB serializer for T. -func NewCodec[T any]() *Codec[T] { return &Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to gob bytes. +func Encoder[T any](v T) ([]byte, error) { buf := sync.Get() defer sync.Put(buf) @@ -25,6 +20,16 @@ func (Codec[T]) Encode(v T) ([]byte, error) { return append([]byte(nil), buf.Bytes()...), nil } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes gob bytes into T. +func Decoder[T any](b []byte, v *T) error { return stdgob.NewDecoder(bytes.NewReader(b)).Decode(v) } + +// NewCodec returns a GOB codec for T. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/gob/codec_test.go b/gob/codec_test.go index c613c2c..21c54d0 100644 --- a/gob/codec_test.go +++ b/gob/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/gob" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } diff --git a/gob/streamcodec.go b/gob/streamcodec.go index a113a17..2ba34d9 100644 --- a/gob/streamcodec.go +++ b/gob/streamcodec.go @@ -3,19 +3,25 @@ package gob import ( "encoding/gob" "io" -) - -// StreamCodec is a StreamCodec[T] backed by encoding/gob. -// It is safe for concurrent use. -type StreamCodec[T any] struct{} -// NewStreamCodec returns a GOB stream serializer for T. -func NewStreamCodec[T any]() *StreamCodec[T] { return &StreamCodec[T]{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec[T]) Encode(w io.Writer, v T) error { +// StreamEncoder encodes T to a gob stream. +func StreamEncoder[T any](w io.Writer, v T) error { return gob.NewEncoder(w).Encode(v) } -func (StreamCodec[T]) Decode(r io.Reader, v *T) error { +// StreamDecoder decodes T from a gob stream. +func StreamDecoder[T any](r io.Reader, v *T) error { return gob.NewDecoder(r).Decode(v) } + +// NewStreamCodec returns a GOB stream codec for T. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/gob/streamcodec_test.go b/gob/streamcodec_test.go index c85e3e9..43092f4 100644 --- a/gob/streamcodec_test.go +++ b/gob/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/gob" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { type Data struct { Name string } diff --git a/gzip/benchmark_test.go b/gzip/benchmark_test.go index 060370e..dbd1e00 100644 --- a/gzip/benchmark_test.go +++ b/gzip/benchmark_test.go @@ -3,13 +3,14 @@ package gzip_test import ( "testing" + goencode "github.com/foomo/goencode" "github.com/foomo/goencode/gzip" "github.com/foomo/goencode/internal/testdata" json "github.com/foomo/goencode/json/v1" ) func BenchmarkCodec(b *testing.B) { - c := gzip.NewCodec(json.NewCodec[*testdata.User]()) + c := goencode.PipeCodec(json.NewCodec[*testdata.User](), gzip.NewCodec()) b.Run("encode", func(b *testing.B) { v := testdata.NewUser() diff --git a/gzip/codec.go b/gzip/codec.go index 26acc14..fbba267 100644 --- a/gzip/codec.go +++ b/gzip/codec.go @@ -10,16 +10,8 @@ import ( "github.com/foomo/goencode/internal/sync" ) -// Codec is a Codec[T] that applies gzip compression on top of another Codec[T]. -// It is safe for concurrent use. -type Codec[T any] struct { - codec encoding.Codec[T] - level int - maxDecodedSize int64 -} - -// NewCodec returns a gzip compression codec that delegates serialization to codec. -func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { +// NewEncoder returns a gzip compression encoder. +func NewEncoder(opts ...Option) encoding.Encoder[[]byte, []byte] { o := options{ level: gzip.DefaultCompression, } @@ -27,58 +19,66 @@ func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { opt(&o) } - return &Codec[T]{ - codec: codec, - level: o.level, - maxDecodedSize: o.maxDecodedSize, - } -} + return func(data []byte) ([]byte, error) { + buf := sync.Get() + defer sync.Put(buf) -func (c *Codec[T]) Encode(v T) ([]byte, error) { - b, err := c.codec.Encode(v) - if err != nil { - return nil, err - } + w, err := gzip.NewWriterLevel(buf, o.level) + if err != nil { + return nil, err + } - buf := sync.Get() - defer sync.Put(buf) + if _, err := w.Write(data); err != nil { + return nil, err + } - w, err := gzip.NewWriterLevel(buf, c.level) - if err != nil { - return nil, err - } + if err := w.Close(); err != nil { + return nil, err + } - if _, err := w.Write(b); err != nil { - return nil, err + return append([]byte(nil), buf.Bytes()...), nil } +} - if err := w.Close(); err != nil { - return nil, err +// NewDecoder returns a gzip decompression decoder. +func NewDecoder(opts ...Option) encoding.Decoder[[]byte, []byte] { + o := options{} + for _, opt := range opts { + opt(&o) } - return append([]byte(nil), buf.Bytes()...), nil -} + return func(data []byte, v *[]byte) error { + r, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return err + } + defer r.Close() -func (c *Codec[T]) Decode(b []byte, v *T) error { - r, err := gzip.NewReader(bytes.NewReader(b)) - if err != nil { - return err - } - defer r.Close() + var src io.Reader = r + if o.maxDecodedSize > 0 { + src = io.LimitReader(r, o.maxDecodedSize+1) + } - var src io.Reader = r - if c.maxDecodedSize > 0 { - src = io.LimitReader(r, c.maxDecodedSize+1) - } + decoded, err := io.ReadAll(src) + if err != nil { + return err + } - data, err := io.ReadAll(src) - if err != nil { - return err - } + if o.maxDecodedSize > 0 && int64(len(decoded)) > o.maxDecodedSize { + return fmt.Errorf("gzip: decompressed size exceeds limit of %d bytes", o.maxDecodedSize) + } - if c.maxDecodedSize > 0 && int64(len(data)) > c.maxDecodedSize { - return fmt.Errorf("gzip: decompressed size exceeds limit of %d bytes", c.maxDecodedSize) + *v = decoded + + return nil } +} - return c.codec.Decode(data, v) +// NewCodec returns a gzip compression codec. +// It is safe for concurrent use. +func NewCodec(opts ...Option) encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: NewEncoder(opts...), + Decode: NewDecoder(opts...), + } } diff --git a/gzip/codec_test.go b/gzip/codec_test.go index 54f433b..edaf637 100644 --- a/gzip/codec_test.go +++ b/gzip/codec_test.go @@ -3,16 +3,17 @@ package gzip_test import ( "fmt" + goencode "github.com/foomo/goencode" "github.com/foomo/goencode/gzip" json "github.com/foomo/goencode/json/v1" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } - c := gzip.NewCodec(json.NewCodec[Data]()) + c := goencode.PipeCodec(json.NewCodec[Data](), gzip.NewCodec()) encoded, err := c.Encode(Data{Name: "example-123"}) if err != nil { diff --git a/gzip/streamcodec.go b/gzip/streamcodec.go index 7b91b8d..db357b3 100644 --- a/gzip/streamcodec.go +++ b/gzip/streamcodec.go @@ -2,21 +2,14 @@ package gzip import ( "compress/gzip" + "fmt" "io" encoding "github.com/foomo/goencode" ) -// StreamCodec is a StreamCodec[T] that applies gzip compression on top of another StreamCodec[T]. -// It is safe for concurrent use. -type StreamCodec[T any] struct { - codec encoding.StreamCodec[T] - level int - maxDecodedSize int64 -} - -// NewStreamCodec returns a gzip compression stream codec that delegates serialization to codec. -func NewStreamCodec[T any](codec encoding.StreamCodec[T], opts ...Option) *StreamCodec[T] { +// NewStreamEncoder returns a gzip compression stream encoder. +func NewStreamEncoder(opts ...Option) encoding.StreamEncoder[[]byte] { o := options{ level: gzip.DefaultCompression, } @@ -24,38 +17,60 @@ func NewStreamCodec[T any](codec encoding.StreamCodec[T], opts ...Option) *Strea opt(&o) } - return &StreamCodec[T]{ - codec: codec, - level: o.level, - maxDecodedSize: o.maxDecodedSize, + return func(w io.Writer, data []byte) error { + gw, err := gzip.NewWriterLevel(w, o.level) + if err != nil { + return err + } + + if _, err := gw.Write(data); err != nil { + gw.Close() + return err + } + + return gw.Close() } } -func (c *StreamCodec[T]) Encode(w io.Writer, v T) error { - gw, err := gzip.NewWriterLevel(w, c.level) - if err != nil { - return err +// NewStreamDecoder returns a gzip decompression stream decoder. +func NewStreamDecoder(opts ...Option) encoding.StreamDecoder[[]byte] { + o := options{} + for _, opt := range opts { + opt(&o) } - if err := c.codec.Encode(gw, v); err != nil { - gw.Close() - return err - } + return func(r io.Reader, v *[]byte) error { + gr, err := gzip.NewReader(r) + if err != nil { + return err + } + defer gr.Close() - return gw.Close() -} + var src io.Reader = gr + if o.maxDecodedSize > 0 { + src = io.LimitReader(gr, o.maxDecodedSize+1) + } -func (c *StreamCodec[T]) Decode(r io.Reader, v *T) error { - gr, err := gzip.NewReader(r) - if err != nil { - return err - } - defer gr.Close() + data, err := io.ReadAll(src) + if err != nil { + return err + } + + if o.maxDecodedSize > 0 && int64(len(data)) > o.maxDecodedSize { + return fmt.Errorf("gzip: decompressed size exceeds limit of %d bytes", o.maxDecodedSize) + } + + *v = data - var src io.Reader = gr - if c.maxDecodedSize > 0 { - src = io.LimitReader(gr, c.maxDecodedSize+1) + return nil } +} - return c.codec.Decode(src, v) +// NewStreamCodec returns a gzip compression stream codec. +// It is safe for concurrent use. +func NewStreamCodec(opts ...Option) encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: NewStreamEncoder(opts...), + Decode: NewStreamDecoder(opts...), + } } diff --git a/gzip/streamcodec_test.go b/gzip/streamcodec_test.go index ee02d2b..a2e2537 100644 --- a/gzip/streamcodec_test.go +++ b/gzip/streamcodec_test.go @@ -5,29 +5,26 @@ import ( "fmt" "github.com/foomo/goencode/gzip" - json "github.com/foomo/goencode/json/v1" ) -func ExampleStreamCodec() { - type Data struct { - Name string - } +func ExampleNewStreamCodec() { + c := gzip.NewStreamCodec() - c := gzip.NewStreamCodec(json.NewStreamCodec[Data]()) + input := []byte("hello gzip stream") var buf bytes.Buffer - if err := c.Encode(&buf, Data{Name: "example-123"}); err != nil { + if err := c.Encode(&buf, input); err != nil { fmt.Printf("Encode failed: %v\n", err) return } - var decoded Data + var decoded []byte if err := c.Decode(&buf, &decoded); err != nil { fmt.Printf("Decode failed: %v\n", err) return } - fmt.Printf("Decoded Name: %s\n", decoded.Name) + fmt.Printf("Decoded: %s\n", string(decoded)) // Output: - // Decoded Name: example-123 + // Decoded: hello gzip stream } diff --git a/hex/codec.go b/hex/codec.go index 0a60abf..36d41fb 100644 --- a/hex/codec.go +++ b/hex/codec.go @@ -2,23 +2,20 @@ package hex import ( stdhex "encoding/hex" -) - -// Codec is a Codec[[]byte] backed by encoding/hex. -// It is safe for concurrent use. -type Codec struct{} -// NewCodec returns a Hex serializer. -func NewCodec() *Codec { return &Codec{} } + encoding "github.com/foomo/goencode" +) -func (Codec) Encode(v []byte) ([]byte, error) { +// Encoder encodes bytes to hexadecimal. +func Encoder(v []byte) ([]byte, error) { dst := make([]byte, stdhex.EncodedLen(len(v))) stdhex.Encode(dst, v) return dst, nil } -func (Codec) Decode(b []byte, v *[]byte) error { +// Decoder decodes hexadecimal bytes. +func Decoder(b []byte, v *[]byte) error { dst := make([]byte, stdhex.DecodedLen(len(b))) n, err := stdhex.Decode(dst, b) @@ -30,3 +27,12 @@ func (Codec) Decode(b []byte, v *[]byte) error { return nil } + +// NewCodec returns a Hex codec. +// It is safe for concurrent use. +func NewCodec() encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: Encoder, + Decode: Decoder, + } +} diff --git a/hex/codec_test.go b/hex/codec_test.go index f864ea2..b6b942e 100644 --- a/hex/codec_test.go +++ b/hex/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/hex" ) -func ExampleCodec() { +func ExampleNewCodec() { c := hex.NewCodec() encoded, err := c.Encode([]byte("hello")) diff --git a/hex/streamcodec.go b/hex/streamcodec.go index a8b41e6..a6711d0 100644 --- a/hex/streamcodec.go +++ b/hex/streamcodec.go @@ -3,22 +3,18 @@ package hex import ( stdhex "encoding/hex" "io" -) - -// StreamCodec is a StreamCodec[[]byte] backed by encoding/hex. -// It is safe for concurrent use. -type StreamCodec struct{} -// NewStreamCodec returns a Hex stream serializer. -func NewStreamCodec() *StreamCodec { return &StreamCodec{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec) Encode(w io.Writer, v []byte) error { +// StreamEncoder encodes bytes to a hexadecimal stream. +func StreamEncoder(w io.Writer, v []byte) error { _, err := stdhex.NewEncoder(w).Write(v) - return err } -func (StreamCodec) Decode(r io.Reader, v *[]byte) error { +// StreamDecoder decodes bytes from a hexadecimal stream. +func StreamDecoder(r io.Reader, v *[]byte) error { data, err := io.ReadAll(stdhex.NewDecoder(r)) if err != nil { return err @@ -28,3 +24,12 @@ func (StreamCodec) Decode(r io.Reader, v *[]byte) error { return nil } + +// NewStreamCodec returns a Hex stream codec. +// It is safe for concurrent use. +func NewStreamCodec() encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: StreamEncoder, + Decode: StreamDecoder, + } +} diff --git a/hex/streamcodec_test.go b/hex/streamcodec_test.go index 8e57fed..0af9673 100644 --- a/hex/streamcodec_test.go +++ b/hex/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/hex" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { c := hex.NewStreamCodec() var buf bytes.Buffer diff --git a/json/v1/codec.go b/json/v1/codec.go index 1189e74..9330902 100644 --- a/json/v1/codec.go +++ b/json/v1/codec.go @@ -2,19 +2,25 @@ package json import ( "encoding/json" -) - -// Codec is a Codec[T] backed by encoding/json. -// It is safe for concurrent use. -type Codec[T any] struct{} -// NewCodec returns a JSON serializer for T. -func NewCodec[T any]() Codec[T] { return Codec[T]{} } + encoding "github.com/foomo/goencode" +) -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to JSON bytes (v1). +func Encoder[T any](v T) ([]byte, error) { return json.Marshal(v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes JSON bytes into T (v1). +func Decoder[T any](b []byte, v *T) error { return json.Unmarshal(b, v) } + +// NewCodec returns a JSON codec for T. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/json/v1/codec_test.go b/json/v1/codec_test.go index b027512..a7aada7 100644 --- a/json/v1/codec_test.go +++ b/json/v1/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/json/v1" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } diff --git a/json/v1/streamcodec.go b/json/v1/streamcodec.go index facb112..426b772 100644 --- a/json/v1/streamcodec.go +++ b/json/v1/streamcodec.go @@ -3,19 +3,25 @@ package json import ( "encoding/json" "io" -) - -// StreamCodec is a StreamCodec[T] backed by encoding/json. -// It is safe for concurrent use. -type StreamCodec[T any] struct{} -// NewStreamCodec returns a JSON stream serializer for T. -func NewStreamCodec[T any]() *StreamCodec[T] { return &StreamCodec[T]{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec[T]) Encode(w io.Writer, v T) error { +// StreamEncoder encodes T to a JSON stream. +func StreamEncoder[T any](w io.Writer, v T) error { return json.NewEncoder(w).Encode(v) } -func (StreamCodec[T]) Decode(r io.Reader, v *T) error { +// StreamDecoder decodes T from a JSON stream. +func StreamDecoder[T any](r io.Reader, v *T) error { return json.NewDecoder(r).Decode(v) } + +// NewStreamCodec returns a JSON stream codec for T. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/json/v1/streamcodec_test.go b/json/v1/streamcodec_test.go index a11bf5f..4e4c9f1 100644 --- a/json/v1/streamcodec_test.go +++ b/json/v1/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/json/v1" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { type Data struct { Name string } diff --git a/json/v2/codec.go b/json/v2/codec.go index aad5041..3f9fcd5 100644 --- a/json/v2/codec.go +++ b/json/v2/codec.go @@ -1,31 +1,25 @@ package json import ( - "io" - + encoding "github.com/foomo/goencode" "github.com/go-json-experiment/json" ) -// Codec is a Codec[T] backed by encoding/json. -// It is zero-allocation on the encode path for small structs and safe for -// concurrent use. -type Codec[T any] struct{} - -// NewCodec returns a JSON serializer for T. -func NewCodec[T any]() *Codec[T] { return &Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to JSON bytes (v2). +func Encoder[T any](v T) ([]byte, error) { return json.Marshal(v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes JSON bytes into T (v2). +func Decoder[T any](b []byte, v *T) error { return json.Unmarshal(b, v) } -func (Codec[T]) EncodeTo(w io.Writer, v T) error { - return json.MarshalWrite(w, v) -} - -func (Codec[T]) DecodeFrom(r io.Reader, v *T) error { - return json.UnmarshalRead(r, v) +// NewCodec returns a JSON codec for T backed by go-json-experiment/json. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } } diff --git a/json/v2/codec_test.go b/json/v2/codec_test.go index 21aa8e2..02cea67 100644 --- a/json/v2/codec_test.go +++ b/json/v2/codec_test.go @@ -6,7 +6,7 @@ import ( json "github.com/foomo/goencode/json/v2" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } diff --git a/json/v2/streamcodec.go b/json/v2/streamcodec.go new file mode 100644 index 0000000..e199202 --- /dev/null +++ b/json/v2/streamcodec.go @@ -0,0 +1,27 @@ +package json + +import ( + "io" + + encoding "github.com/foomo/goencode" + "github.com/go-json-experiment/json" +) + +// StreamEncoder encodes T to a JSON stream (v2). +func StreamEncoder[T any](w io.Writer, v T) error { + return json.MarshalWrite(w, v) +} + +// StreamDecoder decodes T from a JSON stream (v2). +func StreamDecoder[T any](r io.Reader, v *T) error { + return json.UnmarshalRead(r, v) +} + +// NewStreamCodec returns a JSON stream codec for T backed by go-json-experiment/json. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/json/v2/streamcodec_test.go b/json/v2/streamcodec_test.go new file mode 100644 index 0000000..4e85ee7 --- /dev/null +++ b/json/v2/streamcodec_test.go @@ -0,0 +1,32 @@ +package json_test + +import ( + "bytes" + "fmt" + + json "github.com/foomo/goencode/json/v2" +) + +func ExampleNewStreamCodec() { + type Data struct { + Name string + } + + c := json.NewStreamCodec[Data]() + + var buf bytes.Buffer + if err := c.Encode(&buf, Data{Name: "example-123"}); err != nil { + fmt.Printf("Encode failed: %v\n", err) + return + } + + var decoded Data + if err := c.Decode(&buf, &decoded); err != nil { + fmt.Printf("Decode failed: %v\n", err) + return + } + + fmt.Printf("Decoded Name: %s\n", decoded.Name) + // Output: + // Decoded Name: example-123 +} diff --git a/msgpack/tinylib/codec.go b/msgpack/tinylib/codec.go index 4787d2d..fa1c09e 100644 --- a/msgpack/tinylib/codec.go +++ b/msgpack/tinylib/codec.go @@ -3,19 +3,14 @@ package msgpack import ( "fmt" + encoding "github.com/foomo/goencode" "github.com/tinylib/msgp/msgp" ) -// Codec is a Codec[T] backed by tinylib/msgp. +// Encoder encodes T to msgpack bytes (tinylib). // T must have msgp code generation (go:generate msgp) so that -// *T implements msgp.Marshaler and msgp.Unmarshaler. -// It is safe for concurrent use. -type Codec[T any] struct{} - -// NewCodec returns a msgpack serializer for T. -func NewCodec[T any]() *Codec[T] { return &Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// *T implements msgp.Marshaler. +func Encoder[T any](v T) ([]byte, error) { if m, ok := any(v).(msgp.Marshaler); ok { return m.MarshalMsg(nil) } @@ -27,7 +22,10 @@ func (Codec[T]) Encode(v T) ([]byte, error) { return nil, fmt.Errorf("msgpack: %T does not implement msgp.Marshaler", v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes msgpack bytes into T (tinylib). +// T must have msgp code generation (go:generate msgp) so that +// *T implements msgp.Unmarshaler. +func Decoder[T any](b []byte, v *T) error { if u, ok := any(v).(msgp.Unmarshaler); ok { _, err := u.UnmarshalMsg(b) return err @@ -35,3 +33,14 @@ func (Codec[T]) Decode(b []byte, v *T) error { return fmt.Errorf("msgpack: %T does not implement msgp.Unmarshaler", v) } + +// NewCodec returns a msgpack codec for T backed by tinylib/msgp. +// T must have msgp code generation (go:generate msgp) so that +// *T implements msgp.Marshaler and msgp.Unmarshaler. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/msgpack/tinylib/codec_test.go b/msgpack/tinylib/codec_test.go index c5382e9..115a6d4 100644 --- a/msgpack/tinylib/codec_test.go +++ b/msgpack/tinylib/codec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/msgpack/tinylib/testdata" ) -func ExampleCodec() { +func ExampleNewCodec() { c := msgpack.NewCodec[testdata.User]() encoded, err := c.Encode(*testdata.NewUserTinyLib()) diff --git a/msgpack/tinylib/streamcodec.go b/msgpack/tinylib/streamcodec.go index 7332fff..58ab114 100644 --- a/msgpack/tinylib/streamcodec.go +++ b/msgpack/tinylib/streamcodec.go @@ -4,19 +4,14 @@ import ( "fmt" "io" + encoding "github.com/foomo/goencode" "github.com/tinylib/msgp/msgp" ) -// StreamCodec is a StreamCodec[T] backed by tinylib/msgp. +// StreamEncoder encodes T to a msgpack stream (tinylib). // T must have msgp code generation (go:generate msgp) so that -// *T implements msgp.Encodable and msgp.Decodable. -// It is safe for concurrent use. -type StreamCodec[T any] struct{} - -// NewStreamCodec returns a msgpack stream serializer for T. -func NewStreamCodec[T any]() *StreamCodec[T] { return &StreamCodec[T]{} } - -func (StreamCodec[T]) Encode(w io.Writer, v T) error { +// *T implements msgp.Encodable. +func StreamEncoder[T any](w io.Writer, v T) error { if e, ok := any(v).(msgp.Encodable); ok { return msgp.Encode(w, e) } @@ -28,10 +23,24 @@ func (StreamCodec[T]) Encode(w io.Writer, v T) error { return fmt.Errorf("msgpack: %T does not implement msgp.Encodable", v) } -func (StreamCodec[T]) Decode(r io.Reader, v *T) error { +// StreamDecoder decodes T from a msgpack stream (tinylib). +// T must have msgp code generation (go:generate msgp) so that +// *T implements msgp.Decodable. +func StreamDecoder[T any](r io.Reader, v *T) error { if d, ok := any(v).(msgp.Decodable); ok { return msgp.Decode(r, d) } return fmt.Errorf("msgpack: %T does not implement msgp.Decodable", v) } + +// NewStreamCodec returns a msgpack stream codec for T backed by tinylib/msgp. +// T must have msgp code generation (go:generate msgp) so that +// *T implements msgp.Encodable and msgp.Decodable. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/msgpack/tinylib/streamcodec_test.go b/msgpack/tinylib/streamcodec_test.go index 9cc9302..1ee71ca 100644 --- a/msgpack/tinylib/streamcodec_test.go +++ b/msgpack/tinylib/streamcodec_test.go @@ -8,7 +8,7 @@ import ( "github.com/foomo/goencode/msgpack/tinylib/testdata" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { c := msgpack.NewStreamCodec[testdata.User]() var buf bytes.Buffer diff --git a/msgpack/vmihailenco/codec.go b/msgpack/vmihailenco/codec.go index 99a3ecd..142483f 100644 --- a/msgpack/vmihailenco/codec.go +++ b/msgpack/vmihailenco/codec.go @@ -1,20 +1,25 @@ package msgpack import ( + encoding "github.com/foomo/goencode" "github.com/vmihailenco/msgpack/v5" ) -// Codec is a Codec[T] backed by vmihailenco/msgpack/v5. -// It is safe for concurrent use. -type Codec[T any] struct{} - -// NewCodec returns a msgpack serializer for T. -func NewCodec[T any]() *Codec[T] { return &Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to msgpack bytes (vmihailenco). +func Encoder[T any](v T) ([]byte, error) { return msgpack.Marshal(v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes msgpack bytes into T (vmihailenco). +func Decoder[T any](b []byte, v *T) error { return msgpack.Unmarshal(b, v) } + +// NewCodec returns a msgpack codec for T backed by vmihailenco/msgpack/v5. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/msgpack/vmihailenco/codec_test.go b/msgpack/vmihailenco/codec_test.go index 3871c52..b1c453e 100644 --- a/msgpack/vmihailenco/codec_test.go +++ b/msgpack/vmihailenco/codec_test.go @@ -6,7 +6,7 @@ import ( msgpack "github.com/foomo/goencode/msgpack/vmihailenco" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string `msgpack:"name"` } diff --git a/msgpack/vmihailenco/streamcodec.go b/msgpack/vmihailenco/streamcodec.go index 8bd13af..ea3a300 100644 --- a/msgpack/vmihailenco/streamcodec.go +++ b/msgpack/vmihailenco/streamcodec.go @@ -3,20 +3,25 @@ package msgpack import ( "io" + encoding "github.com/foomo/goencode" "github.com/vmihailenco/msgpack/v5" ) -// StreamCodec is a StreamCodec[T] backed by vmihailenco/msgpack/v5. -// It is safe for concurrent use. -type StreamCodec[T any] struct{} - -// NewStreamCodec returns a msgpack stream serializer for T. -func NewStreamCodec[T any]() *StreamCodec[T] { return &StreamCodec[T]{} } - -func (StreamCodec[T]) Encode(w io.Writer, v T) error { +// StreamEncoder encodes T to a msgpack stream (vmihailenco). +func StreamEncoder[T any](w io.Writer, v T) error { return msgpack.NewEncoder(w).Encode(v) } -func (StreamCodec[T]) Decode(r io.Reader, v *T) error { +// StreamDecoder decodes T from a msgpack stream (vmihailenco). +func StreamDecoder[T any](r io.Reader, v *T) error { return msgpack.NewDecoder(r).Decode(v) } + +// NewStreamCodec returns a msgpack stream codec for T backed by vmihailenco/msgpack/v5. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/msgpack/vmihailenco/streamcodec_test.go b/msgpack/vmihailenco/streamcodec_test.go index 218b23e..7d24bcb 100644 --- a/msgpack/vmihailenco/streamcodec_test.go +++ b/msgpack/vmihailenco/streamcodec_test.go @@ -7,7 +7,7 @@ import ( msgpack "github.com/foomo/goencode/msgpack/vmihailenco" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { type Data struct { Name string `msgpack:"name"` } diff --git a/pem/codec.go b/pem/codec.go index a637cfb..fbe31de 100644 --- a/pem/codec.go +++ b/pem/codec.go @@ -3,20 +3,17 @@ package pem import ( stdpem "encoding/pem" "errors" -) - -// Codec is a Codec[*pem.Block] backed by encoding/pem. -// It is safe for concurrent use. -type Codec struct{} -// NewCodec returns a PEM serializer. -func NewCodec() *Codec { return &Codec{} } + encoding "github.com/foomo/goencode" +) -func (Codec) Encode(v *stdpem.Block) ([]byte, error) { +// Encoder encodes a PEM block to bytes. +func Encoder(v *stdpem.Block) ([]byte, error) { return stdpem.EncodeToMemory(v), nil } -func (Codec) Decode(b []byte, v **stdpem.Block) error { +// Decoder decodes bytes into a PEM block. +func Decoder(b []byte, v **stdpem.Block) error { block, _ := stdpem.Decode(b) if block == nil { return errors.New("pem: no PEM block found") @@ -26,3 +23,12 @@ func (Codec) Decode(b []byte, v **stdpem.Block) error { return nil } + +// NewCodec returns a PEM codec. +// It is safe for concurrent use. +func NewCodec() encoding.Codec[*stdpem.Block, []byte] { + return encoding.Codec[*stdpem.Block, []byte]{ + Encode: Encoder, + Decode: Decoder, + } +} diff --git a/pem/codec_test.go b/pem/codec_test.go index f1d644c..ae6734c 100644 --- a/pem/codec_test.go +++ b/pem/codec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/pem" ) -func ExampleCodec() { +func ExampleNewCodec() { c := pem.NewCodec() block := &stdpem.Block{ diff --git a/pem/streamcodec.go b/pem/streamcodec.go index b820727..1979660 100644 --- a/pem/streamcodec.go +++ b/pem/streamcodec.go @@ -4,20 +4,17 @@ import ( stdpem "encoding/pem" "errors" "io" -) - -// StreamCodec is a StreamCodec[*pem.Block] backed by encoding/pem. -// It is safe for concurrent use. -type StreamCodec struct{} -// NewStreamCodec returns a PEM stream serializer. -func NewStreamCodec() *StreamCodec { return &StreamCodec{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec) Encode(w io.Writer, v *stdpem.Block) error { +// StreamEncoder encodes a PEM block to a stream. +func StreamEncoder(w io.Writer, v *stdpem.Block) error { return stdpem.Encode(w, v) } -func (StreamCodec) Decode(r io.Reader, v **stdpem.Block) error { +// StreamDecoder decodes a PEM block from a stream. +func StreamDecoder(r io.Reader, v **stdpem.Block) error { data, err := io.ReadAll(r) if err != nil { return err @@ -32,3 +29,12 @@ func (StreamCodec) Decode(r io.Reader, v **stdpem.Block) error { return nil } + +// NewStreamCodec returns a PEM stream codec. +// It is safe for concurrent use. +func NewStreamCodec() encoding.StreamCodec[*stdpem.Block] { + return encoding.StreamCodec[*stdpem.Block]{ + Encode: StreamEncoder, + Decode: StreamDecoder, + } +} diff --git a/pem/streamcodec_test.go b/pem/streamcodec_test.go index 3de8b33..f07d7a1 100644 --- a/pem/streamcodec_test.go +++ b/pem/streamcodec_test.go @@ -8,7 +8,7 @@ import ( "github.com/foomo/goencode/pem" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { c := pem.NewStreamCodec() block := &stdpem.Block{ diff --git a/pipe.go b/pipe.go new file mode 100644 index 0000000..9c24e22 --- /dev/null +++ b/pipe.go @@ -0,0 +1,34 @@ +package goencode + +// PipeEncoder chains two encoders: A → B → C. +func PipeEncoder[A, B, C any](first Encoder[A, B], second Encoder[B, C]) Encoder[A, C] { + return func(a A) (C, error) { + b, err := first(a) + if err != nil { + var zero C + return zero, err + } + + return second(b) + } +} + +// PipeDecoder chains two decoders in reverse: decodes C → B via second, then B → A via first. +func PipeDecoder[A, B, C any](first Decoder[A, B], second Decoder[B, C]) Decoder[A, C] { + return func(c C, a *A) error { + var b B + if err := second(c, &b); err != nil { + return err + } + + return first(b, a) + } +} + +// PipeCodec chains two codecs: Codec[A,B] + Codec[B,C] → Codec[A,C]. +func PipeCodec[A, B, C any](first Codec[A, B], second Codec[B, C]) Codec[A, C] { + return Codec[A, C]{ + Encode: PipeEncoder(first.Encode, second.Encode), + Decode: PipeDecoder(first.Decode, second.Decode), + } +} diff --git a/pipe_test.go b/pipe_test.go new file mode 100644 index 0000000..799d4a0 --- /dev/null +++ b/pipe_test.go @@ -0,0 +1,123 @@ +package goencode_test + +import ( + "fmt" + "strconv" + "testing" + + goencode "github.com/foomo/goencode" +) + +func ExamplePipeCodec() { + intStr := goencode.Codec[int, string]{ + Encode: func(i int) (string, error) { + return strconv.Itoa(i), nil + }, + Decode: func(s string, i *int) error { + v, err := strconv.Atoi(s) + if err != nil { + return err + } + + *i = v + + return nil + }, + } + strBytes := goencode.Codec[string, []byte]{ + Encode: func(s string) ([]byte, error) { + return []byte(s), nil + }, + Decode: func(b []byte, s *string) error { + *s = string(b) + return nil + }, + } + + piped := goencode.PipeCodec(intStr, strBytes) + + encoded, err := piped.Encode(42) + if err != nil { + fmt.Printf("Encode failed: %v\n", err) + return + } + + var decoded int + if err := piped.Decode(encoded, &decoded); err != nil { + fmt.Printf("Decode failed: %v\n", err) + return + } + + fmt.Printf("Encoded: %s\n", string(encoded)) + fmt.Printf("Decoded: %d\n", decoded) + // Output: + // Encoded: 42 + // Decoded: 42 +} + +func ExamplePipeEncoder() { + intToStr := goencode.Encoder[int, string](func(i int) (string, error) { + return strconv.Itoa(i), nil + }) + strToBytes := goencode.Encoder[string, []byte](func(s string) ([]byte, error) { + return []byte(s), nil + }) + + piped := goencode.PipeEncoder(intToStr, strToBytes) + + got, err := piped(42) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Printf("Result: %s\n", string(got)) + // Output: + // Result: 42 +} + +func ExamplePipeDecoder() { + strToInt := goencode.Decoder[int, string](func(s string, i *int) error { + v, err := strconv.Atoi(s) + if err != nil { + return err + } + + *i = v + + return nil + }) + bytesToStr := goencode.Decoder[string, []byte](func(b []byte, s *string) error { + *s = string(b) + return nil + }) + + piped := goencode.PipeDecoder(strToInt, bytesToStr) + + var got int + if err := piped([]byte("42"), &got); err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Printf("Result: %d\n", got) + // Output: + // Result: 42 +} + +func TestPipeEncoder_FirstError(t *testing.T) { + failing := goencode.Encoder[int, string](func(i int) (string, error) { + return "", fmt.Errorf("encode failed") + }) + second := goencode.Encoder[string, []byte](func(s string) ([]byte, error) { + t.Fatal("second encoder should not be called") + return nil, nil + }) + + piped := goencode.PipeEncoder(failing, second) + + _, err := piped(42) + if err == nil { + t.Fatal("expected error") + } +} diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..b1c9c77 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "grill-me": { + "source": "mattpocock/skills", + "sourceType": "github", + "computedHash": "784f0dbb7403b0f00324bce9a112f715342777a0daee7bbb7385f9c6f0a170ea" + } + } +} diff --git a/snappy/benchmark_test.go b/snappy/benchmark_test.go index ead807c..dd78cbc 100644 --- a/snappy/benchmark_test.go +++ b/snappy/benchmark_test.go @@ -3,13 +3,14 @@ package snappy_test import ( "testing" + goencode "github.com/foomo/goencode" "github.com/foomo/goencode/internal/testdata" json "github.com/foomo/goencode/json/v1" "github.com/foomo/goencode/snappy" ) func BenchmarkCodec(b *testing.B) { - c := snappy.NewCodec(json.NewCodec[*testdata.User]()) + c := goencode.PipeCodec(json.NewCodec[*testdata.User](), snappy.NewCodec()) b.Run("encode", func(b *testing.B) { v := testdata.NewUser() diff --git a/snappy/codec.go b/snappy/codec.go index 18ca1e9..bb73247 100644 --- a/snappy/codec.go +++ b/snappy/codec.go @@ -5,33 +5,28 @@ import ( "github.com/golang/snappy" ) -// Codec is a Codec[T] that applies Snappy compression on top of another Codec[T]. -// It is safe for concurrent use. -type Codec[T any] struct { - codec encoding.Codec[T] +// Encoder compresses bytes using Snappy. +func Encoder(data []byte) ([]byte, error) { + return snappy.Encode(nil, data), nil } -// NewCodec returns a Snappy compression codec that delegates serialization to codec. -func NewCodec[T any](codec encoding.Codec[T]) *Codec[T] { - return &Codec[T]{ - codec: codec, - } -} - -func (c *Codec[T]) Encode(v T) ([]byte, error) { - b, err := c.codec.Encode(v) +// Decoder decompresses Snappy bytes. +func Decoder(data []byte, v *[]byte) error { + decoded, err := snappy.Decode(nil, data) if err != nil { - return nil, err + return err } - return snappy.Encode(nil, b), nil + *v = decoded + + return nil } -func (c *Codec[T]) Decode(b []byte, v *T) error { - data, err := snappy.Decode(nil, b) - if err != nil { - return err +// NewCodec returns a Snappy compression codec. +// It is safe for concurrent use. +func NewCodec() encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: Encoder, + Decode: Decoder, } - - return c.codec.Decode(data, v) } diff --git a/snappy/codec_test.go b/snappy/codec_test.go index 921013f..6a3f31c 100644 --- a/snappy/codec_test.go +++ b/snappy/codec_test.go @@ -3,16 +3,17 @@ package snappy_test import ( "fmt" + goencode "github.com/foomo/goencode" json "github.com/foomo/goencode/json/v1" "github.com/foomo/goencode/snappy" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } - c := snappy.NewCodec(json.NewCodec[Data]()) + c := goencode.PipeCodec(json.NewCodec[Data](), snappy.NewCodec()) encoded, err := c.Encode(Data{Name: "example-123"}) if err != nil { diff --git a/snappy/streamcodec.go b/snappy/streamcodec.go index 3244be8..9ce7249 100644 --- a/snappy/streamcodec.go +++ b/snappy/streamcodec.go @@ -7,29 +7,33 @@ import ( "github.com/golang/snappy" ) -// StreamCodec is a StreamCodec[T] that applies Snappy compression on top of another StreamCodec[T]. -// It is safe for concurrent use. -type StreamCodec[T any] struct { - codec encoding.StreamCodec[T] -} - -// NewStreamCodec returns a Snappy compression stream codec that delegates serialization to codec. -func NewStreamCodec[T any](codec encoding.StreamCodec[T]) *StreamCodec[T] { - return &StreamCodec[T]{ - codec: codec, +// StreamEncoder compresses bytes to a Snappy stream. +func StreamEncoder(w io.Writer, data []byte) error { + sw := snappy.NewBufferedWriter(w) + if _, err := sw.Write(data); err != nil { + return err } -} -func (c *StreamCodec[T]) Encode(w io.Writer, v T) error { - sw := snappy.NewBufferedWriter(w) + return sw.Close() +} - if err := c.codec.Encode(sw, v); err != nil { +// StreamDecoder decompresses bytes from a Snappy stream. +func StreamDecoder(r io.Reader, v *[]byte) error { + data, err := io.ReadAll(snappy.NewReader(r)) + if err != nil { return err } - return sw.Close() + *v = data + + return nil } -func (c *StreamCodec[T]) Decode(r io.Reader, v *T) error { - return c.codec.Decode(snappy.NewReader(r), v) +// NewStreamCodec returns a Snappy compression stream codec. +// It is safe for concurrent use. +func NewStreamCodec() encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: StreamEncoder, + Decode: StreamDecoder, + } } diff --git a/snappy/streamcodec_test.go b/snappy/streamcodec_test.go index 8b821f7..f6fcbec 100644 --- a/snappy/streamcodec_test.go +++ b/snappy/streamcodec_test.go @@ -4,30 +4,27 @@ import ( "bytes" "fmt" - json "github.com/foomo/goencode/json/v1" "github.com/foomo/goencode/snappy" ) -func ExampleStreamCodec() { - type Data struct { - Name string - } +func ExampleNewStreamCodec() { + c := snappy.NewStreamCodec() - c := snappy.NewStreamCodec(json.NewStreamCodec[Data]()) + input := []byte("hello snappy stream") var buf bytes.Buffer - if err := c.Encode(&buf, Data{Name: "example-123"}); err != nil { + if err := c.Encode(&buf, input); err != nil { fmt.Printf("Encode failed: %v\n", err) return } - var decoded Data + var decoded []byte if err := c.Decode(&buf, &decoded); err != nil { fmt.Printf("Decode failed: %v\n", err) return } - fmt.Printf("Decoded Name: %s\n", decoded.Name) + fmt.Printf("Decoded: %s\n", string(decoded)) // Output: - // Decoded Name: example-123 + // Decoded: hello snappy stream } diff --git a/streamcodec.go b/streamcodec.go index 672eb3b..6276cc1 100644 --- a/streamcodec.go +++ b/streamcodec.go @@ -1,11 +1,15 @@ package goencode -import ( - "io" -) - -// StreamCodec encodes T to an io.Writer and decodes T from an io.Reader. -type StreamCodec[T any] interface { - Encode(w io.Writer, v T) error - Decode(r io.Reader, v *T) error +import "io" + +// StreamEncoder encodes S into an io.Writer. +type StreamEncoder[S any] func(w io.Writer, s S) error + +// StreamDecoder decodes S from an io.Reader. +type StreamDecoder[S any] func(r io.Reader, s *S) error + +// StreamCodec bundles streaming encode/decode for S. +type StreamCodec[S any] struct { + Encode StreamEncoder[S] + Decode StreamDecoder[S] } diff --git a/streamdecoder.go b/streamdecoder.go deleted file mode 100644 index 0878629..0000000 --- a/streamdecoder.go +++ /dev/null @@ -1,9 +0,0 @@ -package goencode - -import ( - "io" -) - -type StreamDecoder[T any] interface { - Decode(r io.Reader, v *T) error -} diff --git a/streamdecoderfunc.go b/streamdecoderfunc.go deleted file mode 100644 index 7a48827..0000000 --- a/streamdecoderfunc.go +++ /dev/null @@ -1,11 +0,0 @@ -package goencode - -import ( - "io" -) - -type StreamDecoderFunc[T any] func(r io.Reader, v *T) error - -func (f StreamDecoderFunc[T]) Decode(r io.Reader, v *T) error { - return f(r, v) -} diff --git a/streamencoder.go b/streamencoder.go deleted file mode 100644 index f5b6831..0000000 --- a/streamencoder.go +++ /dev/null @@ -1,9 +0,0 @@ -package goencode - -import ( - "io" -) - -type StreamEncoder[T any] interface { - Encode(w io.Writer, v T) error -} diff --git a/streamencoderfunc.go b/streamencoderfunc.go deleted file mode 100644 index 0e06dad..0000000 --- a/streamencoderfunc.go +++ /dev/null @@ -1,11 +0,0 @@ -package goencode - -import ( - "io" -) - -type StreamEncoderFunc[T any] func(w io.Writer, v T) error - -func (f StreamEncoderFunc[T]) Encode(w io.Writer, v T) error { - return f(w, v) -} diff --git a/toml/codec.go b/toml/codec.go index 4dfe70d..3eae915 100644 --- a/toml/codec.go +++ b/toml/codec.go @@ -1,20 +1,26 @@ package toml import ( + encoding "github.com/foomo/goencode" + "github.com/BurntSushi/toml" ) -// Codec is a Codec[T] backed by github.com/BurntSushi/toml. -// It is safe for concurrent use. -type Codec[T any] struct{} - -// NewCodec returns a TOML codec for T. -func NewCodec[T any]() Codec[T] { return Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to TOML bytes. +func Encoder[T any](v T) ([]byte, error) { return toml.Marshal(v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes TOML bytes into T. +func Decoder[T any](b []byte, v *T) error { return toml.Unmarshal(b, v) } + +// NewCodec returns a TOML codec for T. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/toml/codec_test.go b/toml/codec_test.go index 785f11e..5ddfc49 100644 --- a/toml/codec_test.go +++ b/toml/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/toml" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } diff --git a/toml/streamcodec.go b/toml/streamcodec.go index 8f8885a..1bc37f0 100644 --- a/toml/streamcodec.go +++ b/toml/streamcodec.go @@ -3,21 +3,28 @@ package toml import ( "io" + encoding "github.com/foomo/goencode" + "github.com/BurntSushi/toml" ) -// StreamCodec is a StreamCodec[T] backed by github.com/BurntSushi/toml. -// It is safe for concurrent use. -type StreamCodec[T any] struct{} - -// NewStreamCodec returns a TOML stream codec for T. -func NewStreamCodec[T any]() *StreamCodec[T] { return &StreamCodec[T]{} } - -func (StreamCodec[T]) Encode(w io.Writer, v T) error { +// StreamEncoder encodes T to a TOML stream. +func StreamEncoder[T any](w io.Writer, v T) error { return toml.NewEncoder(w).Encode(v) } -func (StreamCodec[T]) Decode(r io.Reader, v *T) error { +// StreamDecoder decodes T from a TOML stream. +func StreamDecoder[T any](r io.Reader, v *T) error { _, err := toml.NewDecoder(r).Decode(v) + return err } + +// NewStreamCodec returns a TOML stream codec for T. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/toml/streamcodec_test.go b/toml/streamcodec_test.go index c2c2981..0596cda 100644 --- a/toml/streamcodec_test.go +++ b/toml/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/toml" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { type Data struct { Name string } diff --git a/xml/codec.go b/xml/codec.go index fd96525..28d121a 100644 --- a/xml/codec.go +++ b/xml/codec.go @@ -4,17 +4,12 @@ import ( "bytes" "encoding/xml" + encoding "github.com/foomo/goencode" "github.com/foomo/goencode/internal/sync" ) -// Codec is a Codec[T] backed by encoding/xml. -// It is safe for concurrent use. -type Codec[T any] struct{} - -// NewCodec returns an XML serializer for T. -func NewCodec[T any]() *Codec[T] { return &Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to XML bytes. +func Encoder[T any](v T) ([]byte, error) { buf := sync.Get() defer sync.Put(buf) @@ -25,6 +20,16 @@ func (Codec[T]) Encode(v T) ([]byte, error) { return append([]byte(nil), buf.Bytes()...), nil } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes XML bytes into T. +func Decoder[T any](b []byte, v *T) error { return xml.NewDecoder(bytes.NewReader(b)).Decode(v) } + +// NewCodec returns an XML codec for T. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/xml/codec_test.go b/xml/codec_test.go index c1304eb..fd3c117 100644 --- a/xml/codec_test.go +++ b/xml/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/xml" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { XMLName struct{} `xml:"data"` Name string `xml:"name"` diff --git a/xml/streamcodec.go b/xml/streamcodec.go index c9e1823..9c7c110 100644 --- a/xml/streamcodec.go +++ b/xml/streamcodec.go @@ -3,19 +3,25 @@ package xml import ( "encoding/xml" "io" -) - -// StreamCodec is a StreamCodec[T] backed by encoding/xml. -// It is safe for concurrent use. -type StreamCodec[T any] struct{} -// NewStreamCodec returns an XML stream serializer for T. -func NewStreamCodec[T any]() *StreamCodec[T] { return &StreamCodec[T]{} } + encoding "github.com/foomo/goencode" +) -func (StreamCodec[T]) Encode(w io.Writer, v T) error { +// StreamEncoder encodes T to an XML stream. +func StreamEncoder[T any](w io.Writer, v T) error { return xml.NewEncoder(w).Encode(v) } -func (StreamCodec[T]) Decode(r io.Reader, v *T) error { +// StreamDecoder decodes T from an XML stream. +func StreamDecoder[T any](r io.Reader, v *T) error { return xml.NewDecoder(r).Decode(v) } + +// NewStreamCodec returns an XML stream codec for T. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/xml/streamcodec_test.go b/xml/streamcodec_test.go index a555ddb..2568e46 100644 --- a/xml/streamcodec_test.go +++ b/xml/streamcodec_test.go @@ -7,7 +7,7 @@ import ( "github.com/foomo/goencode/xml" ) -func ExampleStreamCodec() { +func ExampleNewStreamCodec() { type Data struct { XMLName struct{} `xml:"data"` Name string `xml:"name"` diff --git a/yaml/v2/codec.go b/yaml/v2/codec.go index ca0b56c..9ea2243 100644 --- a/yaml/v2/codec.go +++ b/yaml/v2/codec.go @@ -1,21 +1,25 @@ package yaml import ( + encoding "github.com/foomo/goencode" "go.yaml.in/yaml/v2" ) -// Codec is a Codec[T] backed by encoding/json. -// It is zero-allocation on the encode path for small structs and safe for -// concurrent use. -type Codec[T any] struct{} - -// NewCodec returns a Codec codec for T. -func NewCodec[T any]() Codec[T] { return Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to YAML v2 bytes. +func Encoder[T any](v T) ([]byte, error) { return yaml.Marshal(v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes YAML v2 bytes into T. +func Decoder[T any](b []byte, v *T) error { return yaml.Unmarshal(b, v) } + +// NewCodec returns a YAML v2 codec for T. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/yaml/v2/codec_test.go b/yaml/v2/codec_test.go index 5d89798..8263316 100644 --- a/yaml/v2/codec_test.go +++ b/yaml/v2/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/yaml/v2" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } diff --git a/yaml/v2/streamcodec.go b/yaml/v2/streamcodec.go new file mode 100644 index 0000000..c42f823 --- /dev/null +++ b/yaml/v2/streamcodec.go @@ -0,0 +1,27 @@ +package yaml + +import ( + "io" + + encoding "github.com/foomo/goencode" + "go.yaml.in/yaml/v2" +) + +// StreamEncoder encodes T to a YAML v2 stream. +func StreamEncoder[T any](w io.Writer, v T) error { + return yaml.NewEncoder(w).Encode(v) +} + +// StreamDecoder decodes T from a YAML v2 stream. +func StreamDecoder[T any](r io.Reader, v *T) error { + return yaml.NewDecoder(r).Decode(v) +} + +// NewStreamCodec returns a YAML v2 stream codec for T. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/yaml/v3/codec.go b/yaml/v3/codec.go index 3594cb0..8a94048 100644 --- a/yaml/v3/codec.go +++ b/yaml/v3/codec.go @@ -1,21 +1,25 @@ package yaml import ( + encoding "github.com/foomo/goencode" "go.yaml.in/yaml/v3" ) -// Codec is a Codec[T] backed by encoding/json. -// It is zero-allocation on the encode path for small structs and safe for -// concurrent use. -type Codec[T any] struct{} - -// NewCodec returns a Codec codec for T. -func NewCodec[T any]() Codec[T] { return Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to YAML v3 bytes. +func Encoder[T any](v T) ([]byte, error) { return yaml.Marshal(v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes YAML v3 bytes into T. +func Decoder[T any](b []byte, v *T) error { return yaml.Unmarshal(b, v) } + +// NewCodec returns a YAML v3 codec for T. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/yaml/v3/codec_test.go b/yaml/v3/codec_test.go index 69b4f8f..f733b00 100644 --- a/yaml/v3/codec_test.go +++ b/yaml/v3/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/yaml/v3" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } diff --git a/yaml/v3/streamcodec.go b/yaml/v3/streamcodec.go new file mode 100644 index 0000000..35b5618 --- /dev/null +++ b/yaml/v3/streamcodec.go @@ -0,0 +1,27 @@ +package yaml + +import ( + "io" + + encoding "github.com/foomo/goencode" + "go.yaml.in/yaml/v3" +) + +// StreamEncoder encodes T to a YAML v3 stream. +func StreamEncoder[T any](w io.Writer, v T) error { + return yaml.NewEncoder(w).Encode(v) +} + +// StreamDecoder decodes T from a YAML v3 stream. +func StreamDecoder[T any](r io.Reader, v *T) error { + return yaml.NewDecoder(r).Decode(v) +} + +// NewStreamCodec returns a YAML v3 stream codec for T. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/yaml/v4/codec.go b/yaml/v4/codec.go index 2cb651e..9d16452 100644 --- a/yaml/v4/codec.go +++ b/yaml/v4/codec.go @@ -1,21 +1,25 @@ package yaml import ( + encoding "github.com/foomo/goencode" "go.yaml.in/yaml/v4" ) -// Codec is a Codec[T] backed by encoding/json. -// It is zero-allocation on the encode path for small structs and safe for -// concurrent use. -type Codec[T any] struct{} - -// NewCodec returns a Codec codec for T. -func NewCodec[T any]() Codec[T] { return Codec[T]{} } - -func (Codec[T]) Encode(v T) ([]byte, error) { +// Encoder encodes T to YAML v4 bytes. +func Encoder[T any](v T) ([]byte, error) { return yaml.Marshal(v) } -func (Codec[T]) Decode(b []byte, v *T) error { +// Decoder decodes YAML v4 bytes into T. +func Decoder[T any](b []byte, v *T) error { return yaml.Unmarshal(b, v) } + +// NewCodec returns a YAML v4 codec for T. +// It is safe for concurrent use. +func NewCodec[T any]() encoding.Codec[T, []byte] { + return encoding.Codec[T, []byte]{ + Encode: Encoder[T], + Decode: Decoder[T], + } +} diff --git a/yaml/v4/codec_test.go b/yaml/v4/codec_test.go index 93479ec..2a109bc 100644 --- a/yaml/v4/codec_test.go +++ b/yaml/v4/codec_test.go @@ -6,7 +6,7 @@ import ( "github.com/foomo/goencode/yaml/v4" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } diff --git a/yaml/v4/streamcodec.go b/yaml/v4/streamcodec.go new file mode 100644 index 0000000..ba52b17 --- /dev/null +++ b/yaml/v4/streamcodec.go @@ -0,0 +1,27 @@ +package yaml + +import ( + "io" + + encoding "github.com/foomo/goencode" + "go.yaml.in/yaml/v4" +) + +// StreamEncoder encodes T to a YAML v4 stream. +func StreamEncoder[T any](w io.Writer, v T) error { + return yaml.NewEncoder(w).Encode(v) +} + +// StreamDecoder decodes T from a YAML v4 stream. +func StreamDecoder[T any](r io.Reader, v *T) error { + return yaml.NewDecoder(r).Decode(v) +} + +// NewStreamCodec returns a YAML v4 stream codec for T. +// It is safe for concurrent use. +func NewStreamCodec[T any]() encoding.StreamCodec[T] { + return encoding.StreamCodec[T]{ + Encode: StreamEncoder[T], + Decode: StreamDecoder[T], + } +} diff --git a/zstd/benchmark_test.go b/zstd/benchmark_test.go index 61e160e..6c9a20d 100644 --- a/zstd/benchmark_test.go +++ b/zstd/benchmark_test.go @@ -3,13 +3,14 @@ package zstd_test import ( "testing" + goencode "github.com/foomo/goencode" "github.com/foomo/goencode/internal/testdata" - "github.com/foomo/goencode/json/v1" + json "github.com/foomo/goencode/json/v1" "github.com/foomo/goencode/zstd" ) func BenchmarkCodec(b *testing.B) { - c := zstd.NewCodec(json.NewCodec[*testdata.User]()) + c := goencode.PipeCodec(json.NewCodec[*testdata.User](), zstd.NewCodec()) b.Run("encode", func(b *testing.B) { v := testdata.NewUser() diff --git a/zstd/codec.go b/zstd/codec.go index c24c8da..b01bbd7 100644 --- a/zstd/codec.go +++ b/zstd/codec.go @@ -5,16 +5,8 @@ import ( "github.com/klauspost/compress/zstd" ) -// Codec is a Codec[T] that applies Zstandard compression on top of another Codec[T]. -// It is safe for concurrent use. -type Codec[T any] struct { - codec encoding.Codec[T] - level zstd.EncoderLevel - maxDecodedSize int64 -} - -// NewCodec returns a Zstandard compression codec that delegates serialization to codec. -func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { +// NewEncoder returns a Zstandard compression encoder. +func NewEncoder(opts ...Option) encoding.Encoder[[]byte, []byte] { o := options{ level: zstd.SpeedDefault, } @@ -22,44 +14,52 @@ func NewCodec[T any](codec encoding.Codec[T], opts ...Option) *Codec[T] { opt(&o) } - return &Codec[T]{ - codec: codec, - level: o.level, - maxDecodedSize: o.maxDecodedSize, + return func(data []byte) ([]byte, error) { + enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(o.level)) + if err != nil { + return nil, err + } + defer enc.Close() + + return enc.EncodeAll(data, nil), nil } } -func (c *Codec[T]) Encode(v T) ([]byte, error) { - b, err := c.codec.Encode(v) - if err != nil { - return nil, err +// NewDecoder returns a Zstandard decompression decoder. +func NewDecoder(opts ...Option) encoding.Decoder[[]byte, []byte] { + o := options{} + for _, opt := range opts { + opt(&o) } - enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(c.level)) - if err != nil { - return nil, err - } - defer enc.Close() + return func(data []byte, v *[]byte) error { + dopts := []zstd.DOption{} + if o.maxDecodedSize > 0 { + dopts = append(dopts, zstd.WithDecoderMaxMemory(uint64(o.maxDecodedSize))) + } - return enc.EncodeAll(b, nil), nil -} + dec, err := zstd.NewReader(nil, dopts...) + if err != nil { + return err + } + defer dec.Close() -func (c *Codec[T]) Decode(b []byte, v *T) error { - opts := []zstd.DOption{} - if c.maxDecodedSize > 0 { - opts = append(opts, zstd.WithDecoderMaxMemory(uint64(c.maxDecodedSize))) - } + decoded, err := dec.DecodeAll(data, nil) + if err != nil { + return err + } - dec, err := zstd.NewReader(nil, opts...) - if err != nil { - return err - } - defer dec.Close() + *v = decoded - data, err := dec.DecodeAll(b, nil) - if err != nil { - return err + return nil } +} - return c.codec.Decode(data, v) +// NewCodec returns a Zstandard compression codec. +// It is safe for concurrent use. +func NewCodec(opts ...Option) encoding.Codec[[]byte, []byte] { + return encoding.Codec[[]byte, []byte]{ + Encode: NewEncoder(opts...), + Decode: NewDecoder(opts...), + } } diff --git a/zstd/codec_test.go b/zstd/codec_test.go index 9108c7d..bbcc835 100644 --- a/zstd/codec_test.go +++ b/zstd/codec_test.go @@ -3,16 +3,17 @@ package zstd_test import ( "fmt" - "github.com/foomo/goencode/json/v1" + goencode "github.com/foomo/goencode" + json "github.com/foomo/goencode/json/v1" "github.com/foomo/goencode/zstd" ) -func ExampleCodec() { +func ExampleNewCodec() { type Data struct { Name string } - c := zstd.NewCodec(json.NewCodec[Data]()) + c := goencode.PipeCodec(json.NewCodec[Data](), zstd.NewCodec()) encoded, err := c.Encode(Data{Name: "example-123"}) if err != nil { diff --git a/zstd/streamcodec.go b/zstd/streamcodec.go index 8368fba..8840d05 100644 --- a/zstd/streamcodec.go +++ b/zstd/streamcodec.go @@ -7,16 +7,8 @@ import ( "github.com/klauspost/compress/zstd" ) -// StreamCodec is a StreamCodec[T] that applies Zstandard compression on top of another StreamCodec[T]. -// It is safe for concurrent use. -type StreamCodec[T any] struct { - codec encoding.StreamCodec[T] - level zstd.EncoderLevel - maxDecodedSize int64 -} - -// NewStreamCodec returns a Zstandard compression stream codec that delegates serialization to codec. -func NewStreamCodec[T any](codec encoding.StreamCodec[T], opts ...Option) *StreamCodec[T] { +// NewStreamEncoder returns a Zstandard compression stream encoder. +func NewStreamEncoder(opts ...Option) encoding.StreamEncoder[[]byte] { o := options{ level: zstd.SpeedDefault, } @@ -24,38 +16,56 @@ func NewStreamCodec[T any](codec encoding.StreamCodec[T], opts ...Option) *Strea opt(&o) } - return &StreamCodec[T]{ - codec: codec, - level: o.level, - maxDecodedSize: o.maxDecodedSize, + return func(w io.Writer, data []byte) error { + zw, err := zstd.NewWriter(w, zstd.WithEncoderLevel(o.level)) + if err != nil { + return err + } + + if _, err := zw.Write(data); err != nil { + zw.Close() + return err + } + + return zw.Close() } } -func (c *StreamCodec[T]) Encode(w io.Writer, v T) error { - zw, err := zstd.NewWriter(w, zstd.WithEncoderLevel(c.level)) - if err != nil { - return err +// NewStreamDecoder returns a Zstandard decompression stream decoder. +func NewStreamDecoder(opts ...Option) encoding.StreamDecoder[[]byte] { + o := options{} + for _, opt := range opts { + opt(&o) } - if err := c.codec.Encode(zw, v); err != nil { - zw.Close() - return err - } + return func(r io.Reader, v *[]byte) error { + dopts := []zstd.DOption{} + if o.maxDecodedSize > 0 { + dopts = append(dopts, zstd.WithDecoderMaxMemory(uint64(o.maxDecodedSize))) + } - return zw.Close() -} + zr, err := zstd.NewReader(r, dopts...) + if err != nil { + return err + } + defer zr.Close() -func (c *StreamCodec[T]) Decode(r io.Reader, v *T) error { - opts := []zstd.DOption{} - if c.maxDecodedSize > 0 { - opts = append(opts, zstd.WithDecoderMaxMemory(uint64(c.maxDecodedSize))) - } + data, err := io.ReadAll(zr) + if err != nil { + return err + } + + *v = data - zr, err := zstd.NewReader(r, opts...) - if err != nil { - return err + return nil } - defer zr.Close() +} - return c.codec.Decode(zr, v) +// NewStreamCodec returns a Zstandard compression stream codec. +// It is safe for concurrent use. +func NewStreamCodec(opts ...Option) encoding.StreamCodec[[]byte] { + return encoding.StreamCodec[[]byte]{ + Encode: NewStreamEncoder(opts...), + Decode: NewStreamDecoder(opts...), + } } diff --git a/zstd/streamcodec_test.go b/zstd/streamcodec_test.go index 33298f4..b638834 100644 --- a/zstd/streamcodec_test.go +++ b/zstd/streamcodec_test.go @@ -4,30 +4,27 @@ import ( "bytes" "fmt" - "github.com/foomo/goencode/json/v1" "github.com/foomo/goencode/zstd" ) -func ExampleStreamCodec() { - type Data struct { - Name string - } +func ExampleNewStreamCodec() { + c := zstd.NewStreamCodec() - c := zstd.NewStreamCodec(json.NewStreamCodec[Data]()) + input := []byte("hello zstd stream") var buf bytes.Buffer - if err := c.Encode(&buf, Data{Name: "example-123"}); err != nil { + if err := c.Encode(&buf, input); err != nil { fmt.Printf("Encode failed: %v\n", err) return } - var decoded Data + var decoded []byte if err := c.Decode(&buf, &decoded); err != nil { fmt.Printf("Decode failed: %v\n", err) return } - fmt.Printf("Decoded Name: %s\n", decoded.Name) + fmt.Printf("Decoded: %s\n", string(decoded)) // Output: - // Decoded Name: example-123 + // Decoded: hello zstd stream }