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