529bb6b532fa — Sean E. Russell 7 years ago
Refactors out common code and adds filter command.
9 files changed, 278 insertions(+), 204 deletions(-)

A => cmd/filter/main.go
M cmd/orgchart/main.go
M cmd/orgchart/highlightrules.go => highlightrules.go
M cmd/orgchart/person.go => person.go
A => process.go
M cmd/orgchart/rank.go => rank.go
M cmd/orgchart/svg.go => svg.go
M cmd/orgchart/svg_test.go => svg_test.go
M vendor/manifest
A => cmd/filter/main.go +65 -0
@@ 0,0 1,65 @@ 
+package main
+
+import (
+	"fmt"
+	"os"
+	"text/template"
+
+	"github.com/droundy/goopt"
+	oc "repos.seanerussell.us/orgchart"
+)
+
+func main() {
+	csv := goopt.String([]string{"-i"}, "", "CSV input file")
+	fltr := goopt.String([]string{"-f"}, "", "Filter for these people's organization (regex)")
+	ands := goopt.Strings([]string{"--ex-and"}, "", "Column,Value -- Exclude (and -- all ands must match")
+	hil := goopt.Strings([]string{"-c"}, "", "Column,ValueMatch,ClassName -- sets a CSS class based on a regexp match in a column")
+	ors := goopt.Strings([]string{"--ex-or"}, "", "Column,Value -- Exclude (or -- any ors may match")
+	incls := goopt.Strings([]string{"--ex-incl"}, "", "Column,Value -- Include (overrides and and or excludes)")
+	templ := goopt.String([]string{"-t", "--templ"}, "{{.Name}} ({{.Manager.Name}})", "Provide a template for the output. Fields are Name string, Manager Person, Reports []Person, Class string (based on -c)")
+
+	goopt.Version = ""
+	goopt.Summary = "Organization chart generator"
+	goopt.Parse(nil)
+
+	if *csv == "" {
+		if len(goopt.Args) == 0 {
+			fmt.Println("CSV is required")
+			goopt.Usage()
+			os.Exit(1)
+		}
+		*csv = goopt.Args[0]
+	}
+	fin, err := os.Open(*csv)
+	defer fin.Close()
+	if err != nil {
+		fmt.Println(err.Error())
+		os.Exit(1)
+	}
+	rules, err := oc.MakeRuleSet(*hil)
+	if err != nil {
+		fmt.Println(err.Error())
+		os.Exit(1)
+	}
+	exc, err := oc.MakeExcludes(*ands, *ors, *incls)
+	if err != nil {
+		fmt.Println(err.Error())
+		os.Exit(1)
+	}
+	ps, err := oc.MakePeople(fin, exc, []int{}, rules)
+	if err != nil {
+		fmt.Println(err.Error())
+		os.Exit(1)
+	}
+	if *fltr != "" {
+		ps = oc.Filter(ps, *fltr)
+	}
+	tmpl, err := template.New("").Parse(*templ)
+	if err != nil {
+		panic(err)
+	}
+	for _, p := range ps {
+		tmpl.Execute(os.Stdout, p)
+		os.Stdout.Write([]byte("\n"))
+	}
+}

          
M cmd/orgchart/main.go +7 -195
@@ 13,17 13,14 @@ package main
 // TODO: Guess at box width based on max string length
 
 import (
-	"encoding/csv"
-	"errors"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
-	"regexp"
 	"strconv"
 	"strings"
 
 	"github.com/droundy/goopt"
+	oc "repos.seanerussell.us/orgchart"
 )
 
 /**************************************************************************

          
@@ 78,25 75,25 @@ func main() {
 			}
 		}
 	}
-	rules, err := makeRuleSet(*hil)
+	rules, err := oc.MakeRuleSet(*hil)
 	if err != nil {
 		fmt.Println(err.Error())
 		os.Exit(1)
 	}
-	exc, err := makeExcludes(*ands, *ors, *incls)
+	exc, err := oc.MakeExcludes(*ands, *ors, *incls)
 	if err != nil {
 		fmt.Println(err.Error())
 		os.Exit(1)
 	}
-	ps, err := makePeople(fin, exc, cols, rules)
+	ps, err := oc.MakePeople(fin, exc, cols, rules)
 	if err != nil {
 		fmt.Println(err.Error())
 		os.Exit(1)
 	}
 	if *fltr != "" {
-		ps = filter(ps, *fltr)
+		ps = oc.Filter(ps, *fltr)
 	}
-	rs := rank(ps)
+	rs := oc.MakeRank(ps)
 	if *sup {
 		found := false
 		for _, k := range rs[0] {

          
@@ 107,190 104,5 @@ func main() {
 		}
 		*sup = *sup && found
 	}
-	renderSVG(os.Stdout, rs, *sup, css)
-}
-
-/**************************************************************************
- *  Utility functions                                                     *
- **************************************************************************/
-
-// Filter organization based on names. The provided filter is interpreted as a
-// regex; anybody who is not this person, or in this person's organization, is
-// filtered out. If pat is not a parsable regex, an error is printed on STDOUT
-// and the input map is returned.
-func filter(ps map[string]*Person, pat string) map[string]*Person {
-	re, e := regexp.Compile(pat)
-	if e != nil {
-		fmt.Fprintf(os.Stderr, "bad regexp: %s", e.Error())
-		return ps
-	}
-	rv := make(map[string]*Person)
-	for k, p := range ps {
-		if matches(p, re) {
-			rv[k] = p
-		}
-	}
-	return rv
-}
-
-// Return true if this person's name, or name in this person's upward
-// hierarchy, matches the regexp
-func matches(p *Person, re *regexp.Regexp) bool {
-	if p == nil {
-		return false
-	}
-	if re.MatchString(p.Name) {
-		return true
-	}
-	return matches(p.Manager, re)
-}
-
-// Parses a CSV data stream, producing a map of people indexed by the person
-// name.  Returns an error if a non-EOF read error is encountered
-func makePeople(f io.Reader, exc Excludes, cols []int, rs Ruleset) (map[string]*Person, error) {
-	r := csv.NewReader(f)
-	r.LazyQuotes = true
-	r.TrailingComma = true
-	ps := make(map[string]*Person)
-	var l []string
-	var e error
-	mask := make([]bool, len(cols))[:]
-	for i, k := range cols {
-		mask[i] = k > -1
-	}
-	header := true
-	for l, e = r.Read(); e == nil; l, e = r.Read() {
-		if header {
-			header = false
-			continue
-		}
-		if l[0] == "" || exc.Match(l) {
-			continue
-		}
-		name := l[0]
-		p, ok := ps[name]
-		if !ok {
-			p = New(name)
-		}
-		mgrName := l[1]
-		if mgrName != "" {
-			m, ok := ps[mgrName]
-			if !ok {
-				m = New(mgrName)
-				m.LineMask = mask
-				ps[mgrName] = m
-			}
-			p.SetManager(m)
-		}
-		p.Class = rs.Class(l)
-		p.Lines = make([]string, len(cols))
-		for i, k := range cols {
-			if mask[i] {
-				p.Lines[i] = l[k]
-			}
-		}
-		p.LineMask = mask
-		ps[name] = p
-	}
-	if e == io.EOF {
-		return ps, nil
-	}
-	erstrng := fmt.Sprintf("%s: \"%#v\"", e.Error(), l)
-	return ps, errors.New(erstrng)
+	oc.RenderSVG(os.Stdout, rs, *sup, css)
 }
-
-// Given a map of people, produces a ranking where rank[0] are people with no
-// managers, rank[1] are people who's managers are in rank[1], and so on.
-func rank(ps map[string]*Person) Ranks {
-	rs := make(Ranks, 0)
-	for _, p := range ps {
-		if rs.contains(p) {
-			continue
-		}
-		for p.Manager != nil && ps[p.Manager.Name] != nil {
-			p = p.Manager
-		}
-		rs = addRecursively(p, rs, 0)
-	}
-	return rs
-}
-
-// Adds a person and their reports to the supplied ranks structure and recurses
-// into the reports, creating new ranks as needed
-func addRecursively(p *Person, rs Ranks, i int) Ranks {
-	if len(rs) <= i {
-		rs = append(rs, make(Rank, 0))
-	}
-	rs[i] = append(rs[i], p)
-	for _, p1 := range p.Reports {
-		rs = addRecursively(p1, rs, i+1)
-	}
-	return rs
-}
-
-type Exclude struct {
-	idx   int
-	match string
-}
-
-type Excludes struct {
-	ands  []Exclude
-	ors   []Exclude
-	incls []Exclude
-}
-
-func (ex Excludes) Match(cols []string) bool {
-	// If any INCLUDEs match, they win
-	for _, e := range ex.incls {
-		if e.idx < len(cols) {
-			if e.match == cols[e.idx] {
-				return false
-			}
-		}
-	}
-	// If any exclude ORs match, exclude
-	for _, e := range ex.ors {
-		if e.idx < len(cols) {
-			if e.match == cols[e.idx] {
-				return true
-			}
-		}
-	}
-	// If all exclude ANDs match, exclude
-	matches := len(ex.ands) > 0
-	for _, e := range ex.ands {
-		if e.idx < len(cols) {
-			if e.match != cols[e.idx] {
-				matches = false
-			}
-		} else {
-			return false
-		}
-	}
-	return matches
-}
-
-func makeExcludes(ands, ors, incls []string) (Excludes, error) {
-	var err error
-	rv := Excludes{}
-	rv.ands, err = breakUp(ands)
-	rv.ors, err = breakUp(ors)
-	rv.incls, err = breakUp(incls)
-	return rv, err
-}
-
-func breakUp(input []string) ([]Exclude, error) {
-	output := make([]Exclude, 0)
-	for _, a := range input {
-		cs := strings.Split(a, ",")
-		if len(cs) != 2 {
-			return output, errors.New("Malformed exclude argument " + a)
-		}
-		idx, err := strconv.Atoi(cs[0])
-		if err != nil {
-			return output, errors.New("Malformed exclude argument " + a)
-		}
-		output = append(output, Exclude{idx, cs[1]})
-	}
-	return output, nil
-}

          
M cmd/orgchart/highlightrules.go => highlightrules.go +2 -2
@@ 1,4 1,4 @@ 
-package main
+package orgchart
 
 import (
 	"errors"

          
@@ 37,7 37,7 @@ func (rs Ruleset) Class(cols []string) s
 // WARN: the strings are expected to be in format:
 // <col#>,<regexp>,<class>
 // but this is not CSV -- commas are separators, and can't be escaped.
-func makeRuleSet(set []string) (Ruleset, error) {
+func MakeRuleSet(set []string) (Ruleset, error) {
 	rv := make(Ruleset, len(set))
 	for i, v := range set {
 		parts := strings.Split(v, ",")

          
M cmd/orgchart/person.go => person.go +1 -1
@@ 1,4 1,4 @@ 
-package main
+package orgchart
 
 import "fmt"
 

          
A => process.go +197 -0
@@ 0,0 1,197 @@ 
+package orgchart
+
+import (
+	"encoding/csv"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+func MakeExcludes(ands, ors, incls []string) (Excludes, error) {
+	var err error
+	rv := Excludes{}
+	rv.ands, err = breakUp(ands)
+	rv.ors, err = breakUp(ors)
+	rv.incls, err = breakUp(incls)
+	return rv, err
+}
+
+func breakUp(input []string) ([]Exclude, error) {
+	output := make([]Exclude, 0)
+	for _, a := range input {
+		cs := strings.Split(a, ",")
+		if len(cs) != 2 {
+			return output, errors.New("Malformed exclude argument " + a)
+		}
+		idx, err := strconv.Atoi(cs[0])
+		if err != nil {
+			return output, errors.New("Malformed exclude argument " + a)
+		}
+		output = append(output, Exclude{idx, cs[1]})
+	}
+	return output, nil
+}
+
+// Parses a CSV data stream, producing a map of people indexed by the person
+// name.  Returns an error if a non-EOF read error is encountered
+func MakePeople(f io.Reader, exc Excludes, cols []int, rs Ruleset) (map[string]*Person, error) {
+	r := csv.NewReader(f)
+	r.LazyQuotes = true
+	r.TrailingComma = true
+	ps := make(map[string]*Person)
+	var l []string
+	var e error
+	mask := make([]bool, len(cols))[:]
+	for i, k := range cols {
+		mask[i] = k > -1
+	}
+	header := true
+	for l, e = r.Read(); e == nil; l, e = r.Read() {
+		if header {
+			header = false
+			continue
+		}
+		if l[0] == "" || exc.Match(l) {
+			continue
+		}
+		name := l[0]
+		p, ok := ps[name]
+		if !ok {
+			p = New(name)
+		}
+		mgrName := l[1]
+		if mgrName != "" {
+			m, ok := ps[mgrName]
+			if !ok {
+				m = New(mgrName)
+				m.LineMask = mask
+				ps[mgrName] = m
+			}
+			p.SetManager(m)
+		}
+		p.Class = rs.Class(l)
+		p.Lines = make([]string, len(cols))
+		for i, k := range cols {
+			if mask[i] {
+				p.Lines[i] = l[k]
+			}
+		}
+		p.LineMask = mask
+		ps[name] = p
+	}
+	if e == io.EOF {
+		return ps, nil
+	}
+	erstrng := fmt.Sprintf("%s: \"%#v\"", e.Error(), l)
+	return ps, errors.New(erstrng)
+}
+
+// Given a map of people, produces a ranking where rank[0] are people with no
+// managers, rank[1] are people who's managers are in rank[1], and so on.
+func MakeRank(ps map[string]*Person) Ranks {
+	rs := make(Ranks, 0)
+	for _, p := range ps {
+		if rs.Contains(p) {
+			continue
+		}
+		for p.Manager != nil && ps[p.Manager.Name] != nil {
+			p = p.Manager
+		}
+		rs = addRecursively(p, rs, 0)
+	}
+	return rs
+}
+
+// Adds a person and their reports to the supplied ranks structure and recurses
+// into the reports, creating new ranks as needed
+func addRecursively(p *Person, rs Ranks, i int) Ranks {
+	if len(rs) <= i {
+		rs = append(rs, make(Rank, 0))
+	}
+	rs[i] = append(rs[i], p)
+	for _, p1 := range p.Reports {
+		rs = addRecursively(p1, rs, i+1)
+	}
+	return rs
+}
+
+type Exclude struct {
+	idx   int
+	match string
+}
+
+type Excludes struct {
+	ands  []Exclude
+	ors   []Exclude
+	incls []Exclude
+}
+
+func (ex Excludes) Match(cols []string) bool {
+	// If any INCLUDEs match, they win
+	for _, e := range ex.incls {
+		if e.idx < len(cols) {
+			if e.match == cols[e.idx] {
+				return false
+			}
+		}
+	}
+	// If any exclude ORs match, exclude
+	for _, e := range ex.ors {
+		if e.idx < len(cols) {
+			if e.match == cols[e.idx] {
+				return true
+			}
+		}
+	}
+	// If all exclude ANDs match, exclude
+	matches := len(ex.ands) > 0
+	for _, e := range ex.ands {
+		if e.idx < len(cols) {
+			if e.match != cols[e.idx] {
+				matches = false
+			}
+		} else {
+			return false
+		}
+	}
+	return matches
+}
+
+/**************************************************************************
+ *  Utility functions                                                     *
+ **************************************************************************/
+
+// Filter organization based on names. The provided filter is interpreted as a
+// regex; anybody who is not this person, or in this person's organization, is
+// filtered out. If pat is not a parsable regex, an error is printed on STDOUT
+// and the input map is returned.
+func Filter(ps map[string]*Person, pat string) map[string]*Person {
+	re, e := regexp.Compile(pat)
+	if e != nil {
+		fmt.Fprintf(os.Stderr, "bad regexp: %s", e.Error())
+		return ps
+	}
+	rv := make(map[string]*Person)
+	for k, p := range ps {
+		if matches(p, re) {
+			rv[k] = p
+		}
+	}
+	return rv
+}
+
+// Return true if this person's name, or name in this person's upward
+// hierarchy, matches the regexp
+func matches(p *Person, re *regexp.Regexp) bool {
+	if p == nil {
+		return false
+	}
+	if re.MatchString(p.Name) {
+		return true
+	}
+	return matches(p.Manager, re)
+}

          
M cmd/orgchart/rank.go => rank.go +2 -2
@@ 1,4 1,4 @@ 
-package main
+package orgchart
 
 import (
 	"fmt"

          
@@ 43,7 43,7 @@ func (ps Rank) Print(o io.Writer) {
 
 type Ranks []Rank
 
-func (rs Ranks) contains(p *Person) bool {
+func (rs Ranks) Contains(p *Person) bool {
 	for _, r := range rs {
 		if r.contains(p) {
 			return true

          
M cmd/orgchart/svg.go => svg.go +2 -2
@@ 1,4 1,4 @@ 
-package main
+package orgchart
 
 import (
 	"io"

          
@@ 64,7 64,7 @@ func setBoxW(rs Ranks) {
 	boxW = maxTextWidth(rs)*charW + padW
 }
 
-func renderSVG(out io.Writer, rs Ranks, sup bool, css string) {
+func RenderSVG(out io.Writer, rs Ranks, sup bool, css string) {
 	if sup {
 		rs = addSuperiors(rs)
 	}

          
M cmd/orgchart/svg_test.go => svg_test.go +1 -1
@@ 1,4 1,4 @@ 
-package main
+package orgchart
 
 import (
 	"bytes"

          
M vendor/manifest +1 -1
@@ 4,7 4,7 @@ 
 		{
 			"importpath": "github.com/ajstarks/svgo",
 			"repository": "https://github.com/ajstarks/svgo",
-			"vcs": "",
+			"vcs": "git",
 			"revision": "672fe547df4e49efc6db67a74391368bcb149b37",
 			"branch": "master",
 			"notests": true