Some concrete implementations are better to be hidden behind contracts, in a specific package. A layer of abstraction can save you later, mostly on unstable projects that often change requirements, or maybe you just want to test an idea, maybe you don’t have the necessary time to finish a task in a way you’d like to.
A good contract will save you. You can “throw” your implementation behind it and come back later to refine it. Sometimes later means in a few years. But if you’re behind a well designed contract, most probably you’re going to alter only the concrete implementation, without touching large areas of the projects.
I had to filter some user input. Some strings had to be HTML escaped, some sanitized to prevent different attacks. I’ve wrapped everything into a package, behind a contract. For escaping I’ve used Go’s html.EscapeString function, while for sanitizing I’ve found the bluemonday package, which is inspired by an OWASP sanitizier for Java.
The contract is clean and simple:
package sanitizer type Sanitizer interface { EscapeString(s string) string SanitizeString(s string, policies ...interface{}) string }
Nothing fancy for EscapeString, it just wraps html.EscapeString. But SanitizeStrings wraps the bluemonday package (in this case, a small part of it). I don’t want too much coupling to bluemonday outside my sanitizer package, at the API level. Maybe one day I decide to rely on another package instead of bluemonday. Also, bluemonday is based on different configurable policies that I wanted to be configurable from outside.
Here’s my version of a configurable concrete implementation hidden behind a contract:
package sanitizer import ( "html" "github.com/microcosm-cc/bluemonday" ) type Sanitizer interface { EscapeString(s string) string SanitizeString(s string, policies ...interface{}) string } type stringSanitizer struct { UGCPolicy *bluemonday.Policy } func (ss *stringSanitizer) EscapeString(s string) string { return html.EscapeString(s) } type URLPolicy struct { Schemes []string } type NoAttrsPolicy struct { } func (ss *stringSanitizer) SanitizeString(s string, policies ...interface{}) string { p := ss.UGCPolicy for _, pol := range policies { switch pol := pol.(type) { case URLPolicy: p = p.AllowURLSchemes(pol.Schemes...) break case NoAttrsPolicy: p = p.AllowNoAttrs().Globally() break } } return p.Sanitize(s) } func New() Sanitizer { return &stringSanitizer{ UGCPolicy: bluemonday.UGCPolicy(), } }
Usage:
package main import ( "sanitizer" "fmt" ) func main() { sanitize := sanitizer.New() s := sanitize.SanitizeString( `input string`, sanitizer.NoAttrsPolicy{}, sanitizer.URLPolicy{Schemes:[]string{"http", "https"}}, ) fmt.Println(s) }