refactor: oop log parser to pkgs
This commit is contained in:
		| @@ -1,38 +0,0 @@ | |||||||
| package main |  | ||||||
|  |  | ||||||
| // You need to run: |  | ||||||
| // go get -u github.com/wcharczuk/go-chart |  | ||||||
|  |  | ||||||
| // type chartReport struct { |  | ||||||
| // 	title         string |  | ||||||
| // 	width, height int |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| // func (s *chartReport) digest(records iterator) error { |  | ||||||
| // 	w := os.Stdout |  | ||||||
|  |  | ||||||
| // 	donut := chart.DonutChart{ |  | ||||||
| // 		Title: s.title, |  | ||||||
| // 		TitleStyle: chart.Style{ |  | ||||||
| // 			FontSize:  35, |  | ||||||
| // 			Show:      true, |  | ||||||
| // 			FontColor: chart.ColorAlternateGreen, |  | ||||||
| // 		}, |  | ||||||
| // 		Width:  s.width, |  | ||||||
| // 		Height: s.height, |  | ||||||
| // 	} |  | ||||||
|  |  | ||||||
| // 	records.each(func(r record) { |  | ||||||
| // 		v := chart.Value{ |  | ||||||
| // 			Label: r.domain + r.page + ": " + strconv.Itoa(r.visits), |  | ||||||
| // 			Value: float64(r.visits), |  | ||||||
| // 			Style: chart.Style{ |  | ||||||
| // 				FontSize: 14, |  | ||||||
| // 			}, |  | ||||||
| // 		} |  | ||||||
|  |  | ||||||
| // 		donut.Values = append(donut.Values, v) |  | ||||||
| // 	}) |  | ||||||
|  |  | ||||||
| // 	return donut.Render(chart.SVG, w) |  | ||||||
| // } |  | ||||||
							
								
								
									
										42
									
								
								interfaces/log-parser/oop/filepipe.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								interfaces/log-parser/oop/filepipe.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | // 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 main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe/group" | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe/parse" | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe/report" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // fromFile generates a default pipeline. | ||||||
|  | // Detects the correct parser by the file extension. | ||||||
|  | // Uses a TextReport and groups by domain. | ||||||
|  | func fromFile(path string) (*pipe.Pipeline, error) { | ||||||
|  | 	f, err := os.Open(path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var src pipe.Iterator | ||||||
|  | 	switch { | ||||||
|  | 	case strings.HasSuffix(path, ".txt"): | ||||||
|  | 		src = parse.FromText(f) | ||||||
|  | 	case strings.HasSuffix(path, ".jsonl"): | ||||||
|  | 		src = parse.FromJSON(f) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return pipe.New( | ||||||
|  | 		src, | ||||||
|  | 		report.AsText(os.Stdout), | ||||||
|  | 		group.By(group.Domain), | ||||||
|  | 	), nil | ||||||
|  | } | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| // 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 main |  | ||||||
|  |  | ||||||
| type filter struct { |  | ||||||
| 	src     iterator |  | ||||||
| 	filters []filterFunc |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func filterBy(fn ...filterFunc) *filter { |  | ||||||
| 	return &filter{filters: fn} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // transform the record |  | ||||||
| func (f *filter) digest(records iterator) error { |  | ||||||
| 	f.src = records |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // each yields only the filtered records |  | ||||||
| func (f *filter) each(yield recordFn) error { |  | ||||||
| 	return f.src.each(func(r record) { |  | ||||||
| 		if !f.check(r) { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		yield(r) |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // check all the filters against the record |  | ||||||
| func (f *filter) check(r record) bool { |  | ||||||
| 	for _, fi := range f.filters { |  | ||||||
| 		if !fi(r) { |  | ||||||
| 			return false |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return true |  | ||||||
| } |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| package main |  | ||||||
|  |  | ||||||
| import "strings" |  | ||||||
|  |  | ||||||
| type filterFunc func(record) bool |  | ||||||
|  |  | ||||||
| func noopFilter(r record) bool { |  | ||||||
| 	return true |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func notUsing(filter filterFunc) filterFunc { |  | ||||||
| 	return func(r record) bool { |  | ||||||
| 		return !filter(r) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func domainExtFilter(domains ...string) filterFunc { |  | ||||||
| 	return func(r record) bool { |  | ||||||
| 		for _, domain := range domains { |  | ||||||
| 			if strings.HasSuffix(r.domain, "."+domain) { |  | ||||||
| 				return true |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func domainFilter(domain string) filterFunc { |  | ||||||
| 	return func(r record) bool { |  | ||||||
| 		return strings.Contains(r.domain, domain) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func orgDomainsFilter(r record) bool { |  | ||||||
| 	return strings.HasSuffix(r.domain, ".org") |  | ||||||
| } |  | ||||||
| @@ -1,49 +0,0 @@ | |||||||
| // 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 main |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"sort" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type group struct { |  | ||||||
| 	sum  map[string]record // metrics per group key |  | ||||||
| 	keys []string          // unique group keys |  | ||||||
| 	key  groupFunc |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func groupBy(key groupFunc) *group { |  | ||||||
| 	return &group{ |  | ||||||
| 		sum: make(map[string]record), |  | ||||||
| 		key: key, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // digest the records |  | ||||||
| func (g *group) digest(records iterator) error { |  | ||||||
| 	return records.each(func(r record) { |  | ||||||
| 		k := g.key(r) |  | ||||||
|  |  | ||||||
| 		if _, ok := g.sum[k]; !ok { |  | ||||||
| 			g.keys = append(g.keys, k) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		g.sum[k] = r.sum(g.sum[k]) |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // each yields the grouped records |  | ||||||
| func (g *group) each(yield recordFn) error { |  | ||||||
| 	sort.Strings(g.keys) |  | ||||||
|  |  | ||||||
| 	for _, k := range g.keys { |  | ||||||
| 		yield(g.sum[k]) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| package main |  | ||||||
|  |  | ||||||
| type groupFunc func(record) string |  | ||||||
|  |  | ||||||
| // domainGrouper groups by domain. |  | ||||||
| // but it keeps the other fields. |  | ||||||
| // for example: it returns pages as well, but you shouldn't use them. |  | ||||||
| // exercise: write a function that erases the unnecessary data. |  | ||||||
| func domainGrouper(r record) string { |  | ||||||
| 	return r.domain |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func pageGrouper(r record) string { |  | ||||||
| 	return r.domain + r.page |  | ||||||
| } |  | ||||||
| @@ -10,35 +10,40 @@ package main | |||||||
| import ( | import ( | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
|  |  | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe/filter" | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe/group" | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe/parse" | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe/report" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	// newGrouper(domainGrouper) | 	pipe := pipe.New( | ||||||
|  | 		parse.FromText(os.Stdin), | ||||||
| 	// s := &chartReport{ | 		// parse.FromJSON(os.Stdin), | ||||||
| 	//  title:  "visits per domain", | 		report.AsText(os.Stdout), | ||||||
| 	//  width:  1920, | 		filter.By(filter.Not(filter.DomainExt("com", "io"))), | ||||||
| 	//  height: 800, | 		group.By(group.Domain), | ||||||
| 	// } | 		new(logger), | ||||||
|  |  | ||||||
| 	// pipe, err := fromFile("../logs/log.jsonl") |  | ||||||
| 	// if err != nil { |  | ||||||
| 	// 	log.Fatalln(err) |  | ||||||
| 	// } |  | ||||||
|  |  | ||||||
| 	pipe := newPipeline( |  | ||||||
| 		newTextLog(os.Stdin), |  | ||||||
| 		// newJSONLog(os.Stdin), |  | ||||||
| 		newTextReport(), |  | ||||||
| 		filterBy(notUsing(domainExtFilter("com", "io"))), |  | ||||||
| 		groupBy(domainGrouper), |  | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
| 	if err := pipe.run(); err != nil { | 	if err := pipe.Run(); err != nil { | ||||||
| 		log.Fatalln(err) | 		log.Fatalln(err) | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
| 	// if err := reportFromFile(os.Args[1]); err != nil { |  | ||||||
| 	// 	log.Fatalln(err) | type logger struct { | ||||||
| 	// } | 	src pipe.Iterator | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l *logger) Digest(records pipe.Iterator) error { | ||||||
|  | 	l.src = records | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (l *logger) Each(yield func(pipe.Record)) error { | ||||||
|  | 	return l.src.Each(func(r pipe.Record) { | ||||||
|  | 		yield(r) | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								interfaces/log-parser/oop/pipe/filter/domain.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								interfaces/log-parser/oop/pipe/filter/domain.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | // 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 filter | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // DomainExt filters a set of domain extensions. | ||||||
|  | func DomainExt(domains ...string) Func { | ||||||
|  | 	return func(r pipe.Record) bool { | ||||||
|  | 		for _, domain := range domains { | ||||||
|  | 			if strings.HasSuffix(r.Str("domain"), "."+domain) { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Domain filters a domain if it contains the given text. | ||||||
|  | func Domain(text string) Func { | ||||||
|  | 	return func(r pipe.Record) bool { | ||||||
|  | 		return strings.Contains(r.Str("domain"), text) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DomainOrg filters only the ".org" domains. | ||||||
|  | func DomainOrg(r pipe.Record) bool { | ||||||
|  | 	return strings.HasSuffix(r.Str("domain"), ".org") | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								interfaces/log-parser/oop/pipe/filter/filter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								interfaces/log-parser/oop/pipe/filter/filter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | // 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 filter | ||||||
|  |  | ||||||
|  | import "github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  |  | ||||||
|  | // Func represents a filtering pipeline func. | ||||||
|  | type Func func(pipe.Record) (pass bool) | ||||||
|  |  | ||||||
|  | // Filter the records. | ||||||
|  | type Filter struct { | ||||||
|  | 	src     pipe.Iterator | ||||||
|  | 	filters []Func | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // By returns a new filter pipeline. | ||||||
|  | func By(fn ...Func) *Filter { | ||||||
|  | 	return &Filter{filters: fn} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Digest saves the iterator for later processing. | ||||||
|  | func (f *Filter) Digest(records pipe.Iterator) error { | ||||||
|  | 	f.src = records | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Each yields only the filtered records. | ||||||
|  | func (f *Filter) Each(yield func(pipe.Record)) error { | ||||||
|  | 	return f.src.Each(func(r pipe.Record) { | ||||||
|  | 		if !f.check(r) { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		yield(r) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // check all the filters against the record. | ||||||
|  | func (f *Filter) check(r pipe.Record) bool { | ||||||
|  | 	for _, fi := range f.filters { | ||||||
|  | 		if !fi(r) { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								interfaces/log-parser/oop/pipe/filter/noop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								interfaces/log-parser/oop/pipe/filter/noop.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | // 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 filter | ||||||
|  |  | ||||||
|  | import "github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  |  | ||||||
|  | // Noop filter that does nothing. | ||||||
|  | func Noop(r pipe.Record) bool { | ||||||
|  | 	return true | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								interfaces/log-parser/oop/pipe/filter/not.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								interfaces/log-parser/oop/pipe/filter/not.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | // 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 filter | ||||||
|  |  | ||||||
|  | import "github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  |  | ||||||
|  | // Not reverses a filter. True becomes false, and vice versa. | ||||||
|  | func Not(filter Func) Func { | ||||||
|  | 	return func(r pipe.Record) bool { | ||||||
|  | 		return !filter(r) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								interfaces/log-parser/oop/pipe/group/domain.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								interfaces/log-parser/oop/pipe/group/domain.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | // 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 group | ||||||
|  |  | ||||||
|  | import "github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  |  | ||||||
|  | // Domain groups the records by domain. | ||||||
|  | // It keeps the other fields intact. | ||||||
|  | // For example: It returns the page field as well. | ||||||
|  | // Exercise: Write a solution that removes the unnecessary data. | ||||||
|  | func Domain(r pipe.Record) string { | ||||||
|  | 	return r.Str("domain") | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								interfaces/log-parser/oop/pipe/group/group.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								interfaces/log-parser/oop/pipe/group/group.go
									
									
									
									
									
										Normal 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 group | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"sort" | ||||||
|  |  | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Func represents a grouping func that returns a grouping key. | ||||||
|  | type Func func(pipe.Record) (key string) | ||||||
|  |  | ||||||
|  | // Group records by a key. | ||||||
|  | type Group struct { | ||||||
|  | 	sum  map[string]pipe.Record // metrics per group key | ||||||
|  | 	keys []string               // unique group keys | ||||||
|  | 	key  Func | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // By returns a new Group. | ||||||
|  | // It takes a group func that returns a group key. | ||||||
|  | // The returned group will group the record using the key. | ||||||
|  | func By(key Func) *Group { | ||||||
|  | 	return &Group{ | ||||||
|  | 		sum: make(map[string]pipe.Record), | ||||||
|  | 		key: key, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Digest records for grouping. | ||||||
|  | func (g *Group) Digest(records pipe.Iterator) error { | ||||||
|  | 	return records.Each(func(r pipe.Record) { | ||||||
|  | 		k := g.key(r) | ||||||
|  |  | ||||||
|  | 		if _, ok := g.sum[k]; !ok { | ||||||
|  | 			g.keys = append(g.keys, k) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if r, ok := r.(pipe.Summer); ok { | ||||||
|  | 			g.sum[k] = r.Sum(g.sum[k]) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Each sorts and yields the grouped records. | ||||||
|  | func (g *Group) Each(yield func(pipe.Record)) error { | ||||||
|  | 	sort.Strings(g.keys) | ||||||
|  |  | ||||||
|  | 	for _, k := range g.keys { | ||||||
|  | 		yield(g.sum[k]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								interfaces/log-parser/oop/pipe/group/page.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								interfaces/log-parser/oop/pipe/group/page.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | // 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 group | ||||||
|  |  | ||||||
|  | import "github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  |  | ||||||
|  | // Page groups records by page. | ||||||
|  | func Page(r pipe.Record) string { | ||||||
|  | 	return r.Str("domain") + r.Str("page") | ||||||
|  | } | ||||||
| @@ -5,18 +5,20 @@ | |||||||
| // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ | ||||||
| // | // | ||||||
| 
 | 
 | ||||||
| package main | package pipe | ||||||
| 
 | 
 | ||||||
| import "fmt" | import "fmt" | ||||||
| 
 | 
 | ||||||
| // logCount counts the yielded records | // logCount counts the yielded records. | ||||||
| type logCount struct { | type logCount struct { | ||||||
| 	iterator | 	Iterator | ||||||
| 	n int | 	n int | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (lc *logCount) each(yield recordFn) error { | // Each yields to the inner iterator while counting the records. | ||||||
| 	err := lc.iterator.each(func(r record) { | // Reports the record number on an error. | ||||||
|  | func (lc *logCount) Each(yield func(Record)) error { | ||||||
|  | 	err := lc.Iterator.Each(func(r Record) { | ||||||
| 		lc.n++ | 		lc.n++ | ||||||
| 		yield(r) | 		yield(r) | ||||||
| 	}) | 	}) | ||||||
| @@ -28,6 +30,7 @@ func (lc *logCount) each(yield recordFn) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // count returns the last read record number. | ||||||
| func (lc *logCount) count() int { | func (lc *logCount) count() int { | ||||||
| 	return lc.n | 	return lc.n | ||||||
| } | } | ||||||
| @@ -5,12 +5,13 @@ | |||||||
| // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ | ||||||
| // | // | ||||||
| 
 | 
 | ||||||
| package main | package parse | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io" | 	"io" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // readClose the reader if it's a io.Closer. | ||||||
| func readClose(r io.Reader) { | func readClose(r io.Reader) { | ||||||
| 	if rc, ok := r.(io.Closer); ok { | 	if rc, ok := r.(io.Closer); ok { | ||||||
| 		rc.Close() | 		rc.Close() | ||||||
| @@ -5,26 +5,30 @@ | |||||||
| // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ | ||||||
| // | // | ||||||
| 
 | 
 | ||||||
| package main | package parse | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bufio" |  | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"io" | 	"io" | ||||||
|  | 
 | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type jsonLog struct { | // JSON parses json records. | ||||||
|  | type JSON struct { | ||||||
| 	reader io.Reader | 	reader io.Reader | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newJSONLog(r io.Reader) *jsonLog { | // FromJSON creates a json parser. | ||||||
| 	return &jsonLog{reader: r} | func FromJSON(r io.Reader) *JSON { | ||||||
|  | 	return &JSON{reader: r} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (j *jsonLog) each(yield recordFn) error { | // Each yields records from a json reader. | ||||||
|  | func (j *JSON) Each(yield func(pipe.Record)) error { | ||||||
| 	defer readClose(j.reader) | 	defer readClose(j.reader) | ||||||
| 
 | 
 | ||||||
| 	dec := json.NewDecoder(bufio.NewReader(j.reader)) | 	dec := json.NewDecoder(j.reader) | ||||||
| 
 | 
 | ||||||
| 	for { | 	for { | ||||||
| 		var r record | 		var r record | ||||||
							
								
								
									
										116
									
								
								interfaces/log-parser/oop/pipe/parse/record.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								interfaces/log-parser/oop/pipe/parse/record.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | |||||||
|  | package parse | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const fieldsLength = 4 | ||||||
|  |  | ||||||
|  | // record stores fields of a log line. | ||||||
|  | type record struct { | ||||||
|  | 	Domain  string | ||||||
|  | 	Page    string | ||||||
|  | 	Visits  int | ||||||
|  | 	Uniques int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Str gets a string field by name. | ||||||
|  | func (r record) Str(field string) string { | ||||||
|  | 	switch field { | ||||||
|  | 	case "domain": | ||||||
|  | 		return r.Domain | ||||||
|  | 	case "page": | ||||||
|  | 		return r.Page | ||||||
|  | 	} | ||||||
|  | 	panic(fieldErr(field)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Int gets an integer field by name. | ||||||
|  | func (r record) Int(field string) int { | ||||||
|  | 	switch field { | ||||||
|  | 	case "visits": | ||||||
|  | 		return r.Visits | ||||||
|  | 	case "uniques": | ||||||
|  | 		return r.Uniques | ||||||
|  | 	} | ||||||
|  | 	panic(fieldErr(field)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Sum the numeric fields with another record. | ||||||
|  | func (r record) Sum(other pipe.Record) pipe.Record { | ||||||
|  | 	if other == nil { | ||||||
|  | 		return r | ||||||
|  | 	} | ||||||
|  | 	r.Visits += other.(record).Visits | ||||||
|  | 	r.Uniques += other.(record).Uniques | ||||||
|  | 	return r | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnmarshalText to a *record. | ||||||
|  | func (r *record) UnmarshalText(p []byte) (err error) { | ||||||
|  | 	fields := strings.Fields(string(p)) | ||||||
|  | 	if len(fields) != fieldsLength { | ||||||
|  | 		return fmt.Errorf("wrong number of fields %q", fields) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	r.Domain, r.Page = fields[0], fields[1] | ||||||
|  |  | ||||||
|  | 	if r.Visits, err = parseStr("visits", fields[2]); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if r.Uniques, err = parseStr("uniques", fields[3]); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return validate(*r) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnmarshalJSON to a *record. | ||||||
|  | func (r *record) UnmarshalJSON(data []byte) error { | ||||||
|  | 	// `methodless` doesn't have any methods including UnmarshalJSON. | ||||||
|  | 	// This trick prevents the stack-overflow (infinite loop). | ||||||
|  | 	type methodless record | ||||||
|  |  | ||||||
|  | 	var m methodless | ||||||
|  | 	if err := json.Unmarshal(data, &m); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Cast back to the record and save. | ||||||
|  | 	*r = record(m) | ||||||
|  |  | ||||||
|  | 	return validate(*r) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // parseStr helps UnmarshalText for string to positive int parsing. | ||||||
|  | func parseStr(name, v string) (int, error) { | ||||||
|  | 	n, err := strconv.Atoi(v) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, fmt.Errorf("Record.UnmarshalText %q: %v", name, err) | ||||||
|  | 	} | ||||||
|  | 	return n, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // validate whether a parsed record is valid or not. | ||||||
|  | func validate(r record) (err error) { | ||||||
|  | 	switch { | ||||||
|  | 	case r.Domain == "": | ||||||
|  | 		err = errors.New("record.domain cannot be empty") | ||||||
|  | 	case r.Page == "": | ||||||
|  | 		err = errors.New("record.page cannot be empty") | ||||||
|  | 	case r.Visits < 0: | ||||||
|  | 		err = errors.New("record.visits cannot be negative") | ||||||
|  | 	case r.Uniques < 0: | ||||||
|  | 		err = errors.New("record.uniques cannot be negative") | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func fieldErr(field string) error { | ||||||
|  | 	return fmt.Errorf("record field: %q does not exist", field) | ||||||
|  | } | ||||||
| @@ -5,22 +5,27 @@ | |||||||
| // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ | ||||||
| // | // | ||||||
| 
 | 
 | ||||||
| package main | package parse | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"io" | 	"io" | ||||||
|  | 
 | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type textLog struct { | // Text parses text based log lines. | ||||||
|  | type Text struct { | ||||||
| 	reader io.Reader | 	reader io.Reader | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newTextLog(r io.Reader) *textLog { | // FromText creates a text parser. | ||||||
| 	return &textLog{reader: r} | func FromText(r io.Reader) *Text { | ||||||
|  | 	return &Text{reader: r} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *textLog) each(yield recordFn) error { | // Each yields records from a text log. | ||||||
|  | func (p *Text) Each(yield func(pipe.Record)) error { | ||||||
| 	defer readClose(p.reader) | 	defer readClose(p.reader) | ||||||
| 
 | 
 | ||||||
| 	in := bufio.NewScanner(p.reader) | 	in := bufio.NewScanner(p.reader) | ||||||
| @@ -32,7 +37,7 @@ func (p *textLog) each(yield recordFn) error { | |||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		yield(*r) | 		yield(r) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return in.Err() | 	return in.Err() | ||||||
							
								
								
									
										26
									
								
								interfaces/log-parser/oop/pipe/pipe.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								interfaces/log-parser/oop/pipe/pipe.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | // 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 pipe | ||||||
|  |  | ||||||
|  | // Iterator yields a record. | ||||||
|  | type Iterator interface { | ||||||
|  | 	Each(func(Record)) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Digester represents a record consumer. | ||||||
|  | type Digester interface { | ||||||
|  | 	Digest(Iterator) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Transform represents both a record consumer and producer. | ||||||
|  | // It has an input and output. | ||||||
|  | // It takes a single record and provides an iterator for all the records. | ||||||
|  | type Transform interface { | ||||||
|  | 	Digester // consumer | ||||||
|  | 	Iterator // producer | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								interfaces/log-parser/oop/pipe/pipeline.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								interfaces/log-parser/oop/pipe/pipeline.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | // 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 pipe | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Pipeline takes records from a source, transforms, and sends them to a destionation. | ||||||
|  | type Pipeline struct { | ||||||
|  | 	src   Iterator | ||||||
|  | 	trans []Transform | ||||||
|  | 	dst   Digester | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // New creates a new pipeline. | ||||||
|  | func New(src Iterator, dst Digester, t ...Transform) *Pipeline { | ||||||
|  | 	return &Pipeline{ | ||||||
|  | 		src:   &logCount{Iterator: src}, | ||||||
|  | 		dst:   dst, | ||||||
|  | 		trans: t, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Run the pipeline. | ||||||
|  | func (p *Pipeline) Run() error { | ||||||
|  | 	defer func() { | ||||||
|  | 		n := p.src.(*logCount).count() | ||||||
|  | 		fmt.Fprintf(os.Stderr, "%d records processed.\n", n) | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	last := p.src | ||||||
|  |  | ||||||
|  | 	for _, t := range p.trans { | ||||||
|  | 		if err := t.Digest(last); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		last = t | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return p.dst.Digest(last) | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								interfaces/log-parser/oop/pipe/record.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								interfaces/log-parser/oop/pipe/record.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | // 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 pipe | ||||||
|  |  | ||||||
|  | // Record provides a generic interface for any sort of records. | ||||||
|  | type Record interface { | ||||||
|  | 	Str(field string) string | ||||||
|  | 	Int(field string) int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Summer provides a method for summing the numeric fields. | ||||||
|  | type Summer interface { | ||||||
|  | 	Sum(Record) Record | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								interfaces/log-parser/oop/pipe/report/chart.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								interfaces/log-parser/oop/pipe/report/chart.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | package report | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | // You need to run: | ||||||
|  | // go get -u github.com/wcharczuk/go-chart | ||||||
|  |  | ||||||
|  | // Chart renders a chart. | ||||||
|  | type Chart struct { | ||||||
|  | 	Title         string | ||||||
|  | 	Width, Height int | ||||||
|  |  | ||||||
|  | 	w io.Writer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AsChart returns a Chart report generator. | ||||||
|  | func AsChart(w io.Writer) *Chart { | ||||||
|  | 	return &Chart{w: w} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Digest generates a chart report. | ||||||
|  | func (c *Chart) Digest(records pipe.Iterator) error { | ||||||
|  | 	w := os.Stdout | ||||||
|  |  | ||||||
|  | 	donut := chart.DonutChart{ | ||||||
|  | 		Title: c.Title, | ||||||
|  | 		TitleStyle: chart.Style{ | ||||||
|  | 			FontSize:  35, | ||||||
|  | 			Show:      true, | ||||||
|  | 			FontColor: chart.ColorAlternateGreen, | ||||||
|  | 		}, | ||||||
|  | 		Width:  c.Width, | ||||||
|  | 		Height: c.Height, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	records.Each(func(r pipe.Record) { | ||||||
|  | 		v := chart.Value{ | ||||||
|  | 			Label: r.Str("domain") + r.Str("page") + ": " + strconv.Itoa(r.Int("visits")), | ||||||
|  | 			Value: float64(r.Int("visits")), | ||||||
|  | 			Style: chart.Style{ | ||||||
|  | 				FontSize: 14, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		donut.Values = append(donut.Values, v) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return donut.Render(chart.SVG, w) | ||||||
|  | } | ||||||
|  | */ | ||||||
							
								
								
									
										64
									
								
								interfaces/log-parser/oop/pipe/report/text.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								interfaces/log-parser/oop/pipe/report/text.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | // 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" | ||||||
|  | 	"io" | ||||||
|  | 	"text/tabwriter" | ||||||
|  |  | ||||||
|  | 	"github.com/inancgumus/learngo/interfaces/log-parser/oop/pipe" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	minWidth = 0 | ||||||
|  | 	tabWidth = 4 | ||||||
|  | 	padding  = 4 | ||||||
|  | 	flags    = 0 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Text report generator. | ||||||
|  | type Text struct { | ||||||
|  | 	w io.Writer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AsText returns a Text report generator. | ||||||
|  | func AsText(w io.Writer) *Text { | ||||||
|  | 	return &Text{w: w} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Digest generates a text report. | ||||||
|  | func (t *Text) Digest(records pipe.Iterator) error { | ||||||
|  | 	w := tabwriter.NewWriter(t.w, minWidth, tabWidth, padding, ' ', flags) | ||||||
|  |  | ||||||
|  | 	write := fmt.Fprintf | ||||||
|  |  | ||||||
|  | 	write(w, "DOMAINS\tPAGES\tVISITS\tUNIQUES\n") | ||||||
|  | 	write(w, "-------\t-----\t------\t-------\n") | ||||||
|  |  | ||||||
|  | 	var total pipe.Record | ||||||
|  |  | ||||||
|  | 	records.Each(func(r pipe.Record) { | ||||||
|  | 		if r, ok := r.(pipe.Summer); ok { | ||||||
|  | 			total = r.Sum(total) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		write(w, "%s\t%s\t%d\t%d\n", | ||||||
|  | 			r.Str("domain"), r.Str("page"), | ||||||
|  | 			r.Int("visits"), r.Int("uniques"), | ||||||
|  | 		) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	write(w, "\t\t\t\n") | ||||||
|  | 	write(w, "%s\t%s\t%d\t%d\n", "TOTAL", "", | ||||||
|  | 		total.Int("visits"), | ||||||
|  | 		total.Int("uniques"), | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	return w.Flush() | ||||||
|  | } | ||||||
| @@ -1,78 +0,0 @@ | |||||||
| // 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 main |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type recordFn func(record) |  | ||||||
|  |  | ||||||
| type iterator interface{ each(recordFn) error } |  | ||||||
| type digester interface{ digest(iterator) error } |  | ||||||
|  |  | ||||||
| type transform interface { |  | ||||||
| 	digester |  | ||||||
| 	iterator |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type pipeline struct { |  | ||||||
| 	src   iterator |  | ||||||
| 	trans []transform |  | ||||||
| 	dst   digester |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (p *pipeline) run() error { |  | ||||||
| 	defer func() { |  | ||||||
| 		n := p.src.(*logCount).count() |  | ||||||
| 		fmt.Printf("%d records processed.\n", n) |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	last := p.src |  | ||||||
|  |  | ||||||
| 	for _, t := range p.trans { |  | ||||||
| 		if err := t.digest(last); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		last = t |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return p.dst.digest(last) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func newPipeline(src iterator, dst digester, t ...transform) *pipeline { |  | ||||||
| 	return &pipeline{ |  | ||||||
| 		src:   &logCount{iterator: src}, |  | ||||||
| 		dst:   dst, |  | ||||||
| 		trans: t, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // fromFile generates a default report |  | ||||||
| func fromFile(path string) (*pipeline, error) { |  | ||||||
| 	f, err := os.Open(path) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var src iterator |  | ||||||
| 	switch { |  | ||||||
| 	case strings.HasSuffix(path, ".txt"): |  | ||||||
| 		src = newTextLog(f) |  | ||||||
| 	case strings.HasSuffix(path, ".jsonl"): |  | ||||||
| 		src = newJSONLog(f) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return newPipeline( |  | ||||||
| 		src, |  | ||||||
| 		newTextReport(), |  | ||||||
| 		groupBy(domainGrouper), |  | ||||||
| 	), nil |  | ||||||
| } |  | ||||||
| @@ -1,82 +0,0 @@ | |||||||
| package main |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const fieldsLength = 4 |  | ||||||
|  |  | ||||||
| type record struct { |  | ||||||
| 	domain  string |  | ||||||
| 	page    string |  | ||||||
| 	visits  int |  | ||||||
| 	uniques int |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (r record) sum(other record) record { |  | ||||||
| 	r.visits += other.visits |  | ||||||
| 	r.uniques += other.uniques |  | ||||||
| 	return r |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // UnmarshalText to a *record |  | ||||||
| func (r *record) UnmarshalText(p []byte) (err error) { |  | ||||||
| 	fields := strings.Fields(string(p)) |  | ||||||
| 	if len(fields) != fieldsLength { |  | ||||||
| 		return fmt.Errorf("wrong number of fields %q", fields) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	r.domain, r.page = fields[0], fields[1] |  | ||||||
|  |  | ||||||
| 	if r.visits, err = parseStr("visits", fields[2]); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	if r.uniques, err = parseStr("uniques", fields[3]); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return validate(*r) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // UnmarshalJSON to a *record |  | ||||||
| func (r *record) UnmarshalJSON(data []byte) error { |  | ||||||
| 	var re struct { |  | ||||||
| 		Domain  string |  | ||||||
| 		Page    string |  | ||||||
| 		Visits  int |  | ||||||
| 		Uniques int |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := json.Unmarshal(data, &re); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	*r = record{re.Domain, re.Page, re.Visits, re.Uniques} |  | ||||||
| 	return validate(*r) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // parseStr helps UnmarshalText for string to positive int parsing |  | ||||||
| func parseStr(name, v string) (int, error) { |  | ||||||
| 	n, err := strconv.Atoi(v) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, fmt.Errorf("record.UnmarshalText %q: %v", name, err) |  | ||||||
| 	} |  | ||||||
| 	return n, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func validate(r record) (err error) { |  | ||||||
| 	switch { |  | ||||||
| 	case r.domain == "": |  | ||||||
| 		err = errors.New("record.domain cannot be empty") |  | ||||||
| 	case r.page == "": |  | ||||||
| 		err = errors.New("record.page cannot be empty") |  | ||||||
| 	case r.visits < 0: |  | ||||||
| 		err = errors.New("record.visits cannot be negative") |  | ||||||
| 	case r.uniques < 0: |  | ||||||
| 		err = errors.New("record.uniques cannot be negative") |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
| @@ -1,49 +0,0 @@ | |||||||
| // 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 main |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"text/tabwriter" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // TODO: make this configurable? or exercise? |  | ||||||
| const ( |  | ||||||
| 	minWidth = 0 |  | ||||||
| 	tabWidth = 4 |  | ||||||
| 	padding  = 4 |  | ||||||
| 	flags    = 0 |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type textReport struct{} |  | ||||||
|  |  | ||||||
| func newTextReport() *textReport { |  | ||||||
| 	return new(textReport) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (s *textReport) digest(records iterator) error { |  | ||||||
| 	w := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, ' ', flags) |  | ||||||
|  |  | ||||||
| 	write := fmt.Fprintf |  | ||||||
|  |  | ||||||
| 	write(w, "DOMAINS\tPAGES\tVISITS\tUNIQUES\n") |  | ||||||
| 	write(w, "-------\t-----\t------\t-------\n") |  | ||||||
|  |  | ||||||
| 	var total record |  | ||||||
| 	records.each(func(r record) { |  | ||||||
| 		total = total.sum(r) |  | ||||||
|  |  | ||||||
| 		write(w, "%s\t%s\t%d\t%d\n", r.domain, r.page, r.visits, r.uniques) |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	write(w, "\t\t\t\n") |  | ||||||
| 	write(w, "%s\t%s\t%d\t%d\n", "TOTAL", "", total.visits, total.uniques) |  | ||||||
|  |  | ||||||
| 	return w.Flush() |  | ||||||
| } |  | ||||||
		Reference in New Issue
	
	Block a user