Skip to content

Commit

Permalink
feat(compressor): added gzip/deflate compression support on request (#12
Browse files Browse the repository at this point in the history
)
  • Loading branch information
cnlangzi authored Jan 4, 2025
1 parent e7af653 commit a419c00
Show file tree
Hide file tree
Showing 14 changed files with 460 additions and 22 deletions.
7 changes: 7 additions & 0 deletions .deepsource.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version = 1

[[analyzers]]
name = "go"

[analyzers.meta]
import_root = "github.com/yaitoo/xun"
11 changes: 11 additions & 0 deletions PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
### Changes
-

### Fixes
-

### Tests
Tasks to complete before merging PR:
- [ ] Ensure unit tests are passing. If not run `make unit-test` to check for any regressions :clipboard:
- [ ] Ensure lint tests are passing. if not run `make lint` to check for any issues
- [ ] Ensure codecov/patch is passing for changes.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Xun [ʃʊn] (pronounced 'shoon'), derived from the Chinese character 迅, signif
[![Go Reference](https://pkg.go.dev/badge/github.com/yaitoo/xun.svg)](https://pkg.go.dev/github.com/yaitoo/xun)
[![Codecov](https://codecov.io/gh/yaitoo/xun/branch/main/graph/badge.svg)](https://codecov.io/gh/yaitoo/xun)
[![GitHub Release](https://img.shields.io/github/v/release/yaitoo/xun)](https://github.com/yaitoo/xun/blob/main/CHANGELOG.md)
[![Go Report Card](https://goreportcard.com/badge/yaitoo/xun)](http://goreportcard.com/report/yaitoo/xun)
[![Go Report Card](https://goreportcard.com/badge/github.com/yaitoo/xun)](https://goreportcard.com/report/github.com/yaitoo/xun)

## Features
- Works with Go's built-in `net/http.ServeMux` router. that was introduced in 1.22. [Routing Enhancements for Go 1.22](https://go.dev/blog/routing-enhancements).
- Works with Go's built-in `net/http.ServeMux` router that was introduced in 1.22. [Routing Enhancements for Go 1.22](https://go.dev/blog/routing-enhancements).
- Works with Go's built-in `html/template`. It is built-in support for Server-Side Rendering (SSR).
- Built-in Form and Validate feature with i18n support.
- Support Page Router in `StaticViewEngine` and `HtmlViewEngine`.
Expand Down
50 changes: 35 additions & 15 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type App struct {
watch bool
watcher *fsnotify.Watcher
interceptor Interceptor
compressors []Compressor
}

// New allocates an App instance and loads all view engines.
Expand Down Expand Up @@ -249,12 +250,14 @@ func (app *App) handleFunc(pattern string, hf HandleFunc, opts []RoutingOption,
app.routes[pattern] = r

app.mux.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) {
rw := app.createWriter(req, w)
defer rw.Close()

ctx := &Context{
req: req,
rw: w,
Routing: *r,
app: app,
interceptor: app.interceptor,
req: req,
rw: rw,
Routing: *r,
app: app,
}

err := r.Next(ctx)
Expand Down Expand Up @@ -319,12 +322,14 @@ func (app *App) HandlePage(pattern string, viewName string, v Viewer) {
app.routes[pattern] = r

app.mux.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) {
rw := app.createWriter(req, w)
defer rw.Close()

ctx := &Context{
req: req,
rw: w,
Routing: *r,
app: app,
interceptor: app.interceptor,
req: req,
rw: rw,
Routing: *r,
app: app,
}

err := r.Next(ctx)
Expand All @@ -342,6 +347,19 @@ func (app *App) HandlePage(pattern string, viewName string, v Viewer) {

}

func (app *App) createWriter(req *http.Request, w http.ResponseWriter) ResponseWriter {
acceptEncoding := req.Header.Get("Accept-Encoding")

stars := strings.ContainsAny(acceptEncoding, "*")

for _, compressor := range app.compressors {
if stars || strings.Contains(acceptEncoding, compressor.AcceptEncoding()) {
return compressor.New(w)
}
}
return &responseWriter{ResponseWriter: w}
}

// HandleFile registers a route handler for serving a file.
//
// This function associates a FileViewer with a given file name
Expand Down Expand Up @@ -379,12 +397,14 @@ func (app *App) HandleFile(name string, v *FileViewer) {
r.Viewers[v.MimeType()] = v

app.mux.HandleFunc(pat, func(w http.ResponseWriter, req *http.Request) {
rw := app.createWriter(req, w)
defer rw.Close()

ctx := &Context{
req: req,
rw: w,
Routing: *r,
app: app,
interceptor: app.interceptor,
req: req,
rw: rw,
Routing: *r,
app: app,
}

err := r.Next(ctx)
Expand Down
15 changes: 15 additions & 0 deletions compressor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package xun

import "net/http"

// Compressor is an interface that defines methods for handling HTTP response compression.
// Implementations of this interface should provide the specific encoding type they support
// and a method to create a new ResponseWriter that applies the compression.
//
// AcceptEncoding returns the encoding type that the compressor supports.
//
// New takes an http.ResponseWriter and returns a ResponseWriter that applies the compression.
type Compressor interface {
AcceptEncoding() string
New(rw http.ResponseWriter) ResponseWriter
}
29 changes: 29 additions & 0 deletions compressor_deflate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package xun

import (
"compress/flate"
"net/http"
)

// DeflateCompressor is a struct that provides functionality for compressing data using the DEFLATE algorithm.
type DeflateCompressor struct {
}

// AcceptEncoding returns the encoding type that the DeflateCompressor supports.
// In this case, it returns the string "deflate".
func (c *DeflateCompressor) AcceptEncoding() string {
return "deflate"
}

// New creates a new deflateResponseWriter that wraps the provided http.ResponseWriter.
// It sets the "Content-Encoding" header to "deflate" and initializes a flate.Writer
// with the default compression level.
func (c *DeflateCompressor) New(rw http.ResponseWriter) ResponseWriter {
rw.Header().Set("Content-Encoding", "deflate")
w, _ := flate.NewWriter(rw, flate.DefaultCompression) //nolint: errcheck because flate.DefaultCompression is a valid compression level

return &deflateResponseWriter{
w: w,
ResponseWriter: rw,
}
}
110 changes: 110 additions & 0 deletions compressor_deflate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package xun

import (
"compress/flate"
"io"
"net/http"
"net/http/httptest"
"testing"
"testing/fstest"

"github.com/stretchr/testify/require"
)

func TestDeflateCompressor(t *testing.T) {
fsys := fstest.MapFS{
"public/skin.css": {
Data: []byte("body { color: red; }"),
},
"pages/index.html": {
Data: []byte("<html><head><title>index</title></head><body></body></html>"),
},
}

m := http.NewServeMux()
srv := httptest.NewServer(m)
defer srv.Close()

app := New(WithMux(m), WithFsys(fsys), WithCompressor(&DeflateCompressor{}))
defer app.Close()

app.Get("/json", func(c *Context) error {
return c.View(map[string]string{"message": "hello"})
})

go app.Start()

var tests = []struct {
name string
acceptEncoding string
contentEncoding string
createReader func(r io.Reader) io.Reader
}{
{
name: "deflate",
acceptEncoding: "deflate",
contentEncoding: "deflate",
createReader: func(r io.Reader) io.Reader {
return flate.NewReader(r)
},
},
{
name: "any",
acceptEncoding: "*",
contentEncoding: "deflate",
createReader: func(r io.Reader) io.Reader {
return flate.NewReader(r)
},
},
{
name: "plain",
acceptEncoding: "",
contentEncoding: "",
createReader: func(r io.Reader) io.Reader {
return r
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, srv.URL+"/skin.css", nil)
require.NoError(t, err)
req.Header.Set("Accept-Encoding", test.acceptEncoding)

resp, err := client.Do(req)
require.NoError(t, err)
require.Equal(t, test.contentEncoding, resp.Header.Get("Content-Encoding"))

buf, err := io.ReadAll(test.createReader(resp.Body))
require.NoError(t, err)
require.Equal(t, fsys["public/skin.css"].Data, buf)

req, err = http.NewRequest(http.MethodGet, srv.URL+"/", nil)
require.NoError(t, err)
req.Header.Set("Accept-Encoding", test.acceptEncoding)

resp, err = client.Do(req)
require.NoError(t, err)
require.Equal(t, test.contentEncoding, resp.Header.Get("Content-Encoding"))

buf, err = io.ReadAll(test.createReader(resp.Body))
require.NoError(t, err)
require.Equal(t, fsys["pages/index.html"].Data, buf)

req, err = http.NewRequest(http.MethodGet, srv.URL+"/json", nil)
require.NoError(t, err)
req.Header.Set("Accept-Encoding", test.acceptEncoding)

resp, err = client.Do(req)
require.NoError(t, err)
require.Equal(t, test.contentEncoding, resp.Header.Get("Content-Encoding"))

data := make(map[string]string)
err = json.NewDecoder(test.createReader(resp.Body)).Decode(&data)
require.NoError(t, err)
require.Equal(t, "hello", data["message"])
})
}

}
28 changes: 28 additions & 0 deletions compressor_gzip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package xun

import (
"compress/gzip"
"net/http"
)

// GzipCompressor is a struct that provides methods for compressing and decompressing data using the Gzip algorithm.
type GzipCompressor struct {
}

// AcceptEncoding returns the encoding type that the GzipCompressor supports.
// In this case, it returns "gzip".
func (c *GzipCompressor) AcceptEncoding() string {
return "gzip"
}

// New creates a new gzipResponseWriter that wraps the provided http.ResponseWriter.
// It sets the "Content-Encoding" header to "gzip" and returns the wrapped writer.
func (c *GzipCompressor) New(rw http.ResponseWriter) ResponseWriter {
rw.Header().Set("Content-Encoding", "gzip")

return &gzipResponseWriter{
w: gzip.NewWriter(rw),
ResponseWriter: rw,
}

}
Loading

0 comments on commit a419c00

Please sign in to comment.