interfaces: refactor

This commit is contained in:
Inanc Gumus
2019-08-19 10:21:11 +03:00
parent b95be49711
commit 158f475a2d
89 changed files with 784 additions and 2 deletions

View File

@@ -0,0 +1,52 @@
// For more tutorials: https://blog.learngoprogramming.com
//
// Copyright © 2018 Inanc Gumus
// Learn Go Programming Course
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
//
package report
import (
"fmt"
)
// Parser parses the log file and generates a summary report.
type Parser struct {
summary *Summary // summarizes the parsing results
lines int // number of parsed lines (for the error messages)
lerr error // the last error occurred
}
// New returns a new parsing state.
func New() *Parser {
return &Parser{summary: newSummary()}
}
// Parse parses a log line and adds it to the summary.
func (p *Parser) Parse(line string) {
// if there was an error do not continue
if p.lerr != nil {
return
}
// chain the parser's error to the result's
res, err := parse(line)
if p.lines++; err != nil {
p.lerr = fmt.Errorf("line #%d: %s", p.lines, err)
return
}
p.summary.update(res)
}
// Summarize summarizes the parsing results.
// Only use it after the parsing is done.
func (p *Parser) Summarize() *Summary {
return p.summary
}
// Err returns the last error encountered
func (p *Parser) Err() error {
return p.lerr
}

View File

@@ -0,0 +1,55 @@
package report_test
import (
"strings"
"testing"
"github.com/inancgumus/learngo/28-methods/logparser-testing/report"
)
func newParser(lines string) *report.Parser {
p := report.New()
p.Parse(lines)
return p
}
func TestParserLineErrs(t *testing.T) {
p := newParser("a.com 1 2")
p.Parse("b.com -1 -1")
want := "#2"
err := p.Err().Error()
if !strings.Contains(err, want) {
t.Errorf("want: %q; got: %q", want, err)
}
}
func TestParserStopsOnErr(t *testing.T) {
p := newParser("a.com 10 20")
p.Parse("b.com -1 -1")
p.Parse("neverparses.com 30 40")
s := p.Summarize()
if want, got := 10, s.Total().Visits; want != got {
t.Errorf("want: %d; got: %d", want, got)
}
}
func TestParserIncorrectFields(t *testing.T) {
tests := []struct {
in, name string
}{
{"a.com", "missing fields"},
{"a.com -1 2", "incorrect visits"},
{"a.com 1 -1", "incorrect time spent"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if p := newParser(tt.in); p.Err() == nil {
t.Errorf("in: %q; got: nil err", tt.in)
}
})
}
}

View File

@@ -0,0 +1,60 @@
// For more tutorials: https://blog.learngoprogramming.com
//
// Copyright © 2018 Inanc Gumus
// Learn Go Programming Course
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
//
package report
import (
"fmt"
"strconv"
"strings"
)
// always put all the related things together as in here
// Result stores metrics for a domain
// it uses the value mechanics,
// because it doesn't have to update anything
type Result struct {
Domain string `json:"domain"`
Visits int `json:"visits"`
TimeSpent int `json:"time_spent"`
// add more metrics if needed
}
// add adds the metrics of another Result to itself and returns a new Result
func (r Result) add(other Result) Result {
return Result{
Domain: r.Domain,
Visits: r.Visits + other.Visits,
TimeSpent: r.TimeSpent + other.TimeSpent,
}
}
// parse parses a single log line
func parse(line string) (r Result, err error) {
fields := strings.Fields(line)
if len(fields) != 3 {
return r, fmt.Errorf("missing fields: %v", fields)
}
f := new(field)
r.Domain = fields[0]
r.Visits = f.atoi("visits", fields[1])
r.TimeSpent = f.atoi("time spent", fields[2])
return r, f.err
}
// field helps for field parsing
type field struct{ err error }
func (f *field) atoi(name, val string) int {
n, err := strconv.Atoi(val)
if n < 0 || err != nil {
f.err = fmt.Errorf("incorrect %s: %q", name, val)
}
return n
}

View File

@@ -0,0 +1,86 @@
// For more tutorials: https://blog.learngoprogramming.com
//
// Copyright © 2018 Inanc Gumus
// Learn Go Programming Course
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
//
package report
import (
"sort"
)
// Summary aggregates the parsing results
type Summary struct {
sum map[string]Result // metrics per domain
domains []string // unique domain names
total Result // total visits for all domains
}
// newSummary constructs and initializes a new summary
// You can't use its methods without pointer mechanics
func newSummary() *Summary {
return &Summary{sum: make(map[string]Result)}
}
// Update updates the report for the given parsing result
func (s *Summary) update(r Result) {
domain := r.Domain
if _, ok := s.sum[domain]; !ok {
s.domains = append(s.domains, domain)
}
// let the result handle the addition
// this allows us to manage the result in once place
// and this way it becomes easily extendable
s.total = s.total.add(r)
s.sum[domain] = r.add(s.sum[domain])
}
// Iterator returns `next()` to detect when the iteration ends,
// and a `cur()` to return the current result.
// iterator iterates sorted by domains.
func (s *Summary) Iterator() (next func() bool, cur func() Result) {
sort.Strings(s.domains)
// remember the last iterated result
var last int
next = func() bool {
defer func() { last++ }()
return len(s.domains) > last
}
cur = func() Result {
// returns a copy so the caller cannot change it
name := s.domains[last-1]
return s.sum[name]
}
return
}
// Total returns the total metrics
func (s *Summary) Total() Result {
return s.total
}
// For the interfaces section
//
// MarshalJSON marshals a report to JSON
// Alternative: unexported embedding
// func (s *Summary) MarshalJSON() ([]byte, error) {
// type total struct {
// *Result
// IgnoreDomain *string `json:"domain,omitempty"`
// }
// return json.Marshal(struct {
// Sum map[string]Result `json:"summary"`
// Domains []string `json:"domains"`
// Total total `json:"total"`
// }{
// Sum: s.sum, Domains: s.domains, Total: total{Result: &s.total},
// })
// }

View File

@@ -0,0 +1,44 @@
package report_test
import (
"testing"
"github.com/inancgumus/learngo/28-methods/logparser-testing/report"
)
func TestSummaryTotal(t *testing.T) {
p := newParser("a.com 1 2")
p.Parse("b.com 3 4")
s := p.Summarize()
want := report.Result{Domain: "", Visits: 4, TimeSpent: 6}
if got := s.Total(); want != got {
t.Errorf("want: %+v; got: %+v", want, got)
}
}
func TestSummaryIterator(t *testing.T) {
p := newParser("a.com 1 2")
p.Parse("a.com 3 4")
p.Parse("b.com 5 6")
s := p.Summarize()
next, cur := s.Iterator()
wants := []report.Result{
{Domain: "a.com", Visits: 4, TimeSpent: 6},
{Domain: "b.com", Visits: 5, TimeSpent: 6},
}
for _, want := range wants {
t.Run(want.Domain, func(t *testing.T) {
if got := next(); !got {
t.Errorf("next(): want: %t; got: %t", true, got)
}
if got := cur(); want != got {
t.Errorf("cur(): want: %+v; got: %+v", want, got)
}
})
}
}