diff options
-rw-r--r-- | auth.go | 8 | ||||
-rw-r--r-- | bommom.go | 271 | ||||
-rw-r--r-- | core.go | 55 | ||||
-rw-r--r-- | core_test.go | 1 | ||||
-rw-r--r-- | store.go | 137 |
5 files changed, 322 insertions, 150 deletions
@@ -23,18 +23,18 @@ type AuthService interface { // always returned. type DummyAuth bool // TODO: what is the best "dummy" abstract base type? -func (da *DummyAuth) CheckLogin(name, pw string) error { +func (da DummyAuth) CheckLogin(name, pw string) error { return nil } -func (da *DummyAuth) NewAccount(name, pw, email string) error { +func (da DummyAuth) NewAccount(name, pw, email string) error { return nil } -func (da *DummyAuth) ChangePassword(name, oldPw, newPw string) error { +func (da DummyAuth) ChangePassword(name, oldPw, newPw string) error { return nil } -func (da *DummyAuth) GetEmail(name string) (string, error) { +func (da DummyAuth) GetEmail(name string) (string, error) { return "example@bommom.com", nil } @@ -3,9 +3,16 @@ package main // CLI for bommom tools. Also used to launch web interface. import ( + "encoding/csv" + "encoding/json" + "encoding/xml" "flag" "fmt" + "io" "log" + "os" + "path" + "strings" ) // Command line flags @@ -14,7 +21,7 @@ var ( fileStorePath = flag.String("path", "./filestore", "path to flat file data store top-level directory") verbose = flag.Bool("verbose", false, "print extra info") helpFlag = flag.Bool("help", false, "print full help info") - outFormat = flag.String("format", "text", "command output format (for 'dump' etc)") + outFormat = flag.String("format", "", "command output format (for 'dump' etc)") ) func main() { @@ -32,6 +39,7 @@ func main() { printUsage() return } + if flag.NArg() < 1 { printUsage() fmt.Println() @@ -41,71 +49,134 @@ func main() { switch flag.Arg(0) { default: log.Fatal("Error: unknown command: ", flag.Arg(0)) - case "load", "serve": + case "load", "serve", "convert": log.Fatal("Error: Unimplemented, sorry") case "init": log.Println("Initializing...") - initCmd() - case "dump": - log.Println("Dumping...") - dumpCmd() - case "list": - listCmd() + initCmd() + case "dump": + dumpCmd() + case "list": + listCmd() } } func initCmd() { - _, err := NewJSONFileBomStore(*fileStorePath) - if err != nil { - log.Fatal(err) - } + jfbs, err := NewJSONFileBomStore(*fileStorePath) + if err != nil { + log.Fatal(err) + } + jfbs, err = OpenJSONFileBomStore(*fileStorePath) + if err != nil { + log.Fatal(err) + } + bs, err := jfbs.GetStub(ShortName("common"), ShortName("gizmo")) + if err == nil { + // dummy BomStub already exists? + return + } + b := makeTestBom() + b.Version = "v001" + bs = &BomStub{Name: "gizmo", + Owner: "common", + Description: "fancy stuff", + HeadVersion: b.Version, + IsPublicView: true, + IsPublicEdit: true} + jfbs.Persist(bs, b, "v001") } func dumpCmd() { - b := makeTestBom() - b.Version = "v001" - bs := &BomStub{Name: "widget", - Owner: "common", - Description: "fancy stuff", - HeadVersion: b.Version, - IsPublicView: true, - IsPublicEdit: true} - jfbs, err := OpenJSONFileBomStore(*fileStorePath) - if err != nil { - log.Fatal(err) - } - jfbs.Persist(bs, b, "v001") + if flag.NArg() != 3 && flag.NArg() != 4 { + log.Fatal("Error: wrong number of arguments (expected user and BOM name, optional file)") + } + userStr := flag.Arg(1) + nameStr := flag.Arg(2) + var outFile io.Writer + outFile = os.Stdout + if flag.NArg() == 4 { + f, err := os.Create(flag.Arg(3)) + if err != nil { + log.Fatal(err) + } + defer f.Close() + outFile = io.Writer(f) + // if no outFormat defined, infer from file extension + if *outFormat == "" { + switch ext := path.Ext(f.Name()); ext { + case "", ".txt", ".text": + // pass + case ".json": + *outFormat = "json" + case ".csv": + *outFormat = "csv" + case ".xml": + *outFormat = "xml" + default: + log.Fatal("Unknown file extention (use -format): " + ext) + } + } + } + + if !isShortName(userStr) || !isShortName(nameStr) { + log.Fatal("Error: not valid ShortName: " + userStr + + " and/or " + nameStr) + } + jfbs, err := OpenJSONFileBomStore(*fileStorePath) + if err != nil { + log.Fatal(err) + } + if auth == nil { + auth = DummyAuth(true) + } + bs, b, err := jfbs.GetHead(ShortName(userStr), ShortName(nameStr)) + if err != nil { + log.Fatal(err) + } + + switch *outFormat { + case "text", "": + DumpBomAsText(bs, b, outFile) + case "json": + DumpBomAsJSON(bs, b, outFile) + case "csv": + DumpBomAsCSV(bs, b, outFile) + case "xml": + DumpBomAsXML(bs, b, outFile) + default: + log.Fatal("Error: unknown/unimplemented format: " + *outFormat) + } } func listCmd() { - jfbs, err := OpenJSONFileBomStore(*fileStorePath) - if err != nil { - log.Fatal(err) - } - var bomStubs []BomStub - if flag.NArg() > 2 { - log.Fatal("Error: too many arguments...") - } - if flag.NArg() == 2 { - name := flag.Arg(1) - if !isShortName(name) { - log.Fatal("Error: not a possible username: " + name) - } - bomStubs, err = jfbs.ListBoms(ShortName(name)) - if err != nil { - log.Fatal(err) - } - } else { - // list all boms from all names - // TODO: ERROR - bomStubs, err = jfbs.ListBoms("") - if err != nil { - log.Fatal(err) - } - } - for _, bs := range bomStubs { - fmt.Println(bs.Owner + "/" + bs.Name) - } + jfbs, err := OpenJSONFileBomStore(*fileStorePath) + if err != nil { + log.Fatal(err) + } + var bomStubs []BomStub + if flag.NArg() > 2 { + log.Fatal("Error: too many arguments...") + } + if flag.NArg() == 2 { + name := flag.Arg(1) + if !isShortName(name) { + log.Fatal("Error: not a possible username: " + name) + } + bomStubs, err = jfbs.ListBoms(ShortName(name)) + if err != nil { + log.Fatal(err) + } + } else { + // list all boms from all names + // TODO: ERROR + bomStubs, err = jfbs.ListBoms("") + if err != nil { + log.Fatal(err) + } + } + for _, bs := range bomStubs { + fmt.Println(bs.Owner + "/" + bs.Name) + } } func printUsage() { @@ -118,11 +189,101 @@ func printUsage() { fmt.Println("") fmt.Println("\tinit \t\t initialize BOM and authentication datastores") fmt.Println("\tlist [user]\t\t list BOMs, optionally filtered by user") - fmt.Println("\tload <file>\t import a BOM") - fmt.Println("\tdump <user> <name>\t dump a BOM to stdout") + fmt.Println("\tload <file.type> [user] [bom_name]\t import a BOM") + fmt.Println("\tdump <user> <name> [file.type]\t dump a BOM to stdout") + fmt.Println("\tconvert <infile.type> [outfile.type]\t convert a BOM file") fmt.Println("\tserve\t\t serve up web interface over HTTP") fmt.Println("") fmt.Println("Extra command line options:") fmt.Println("") flag.PrintDefaults() } + +// -------- conversion/dump/load routines + +func DumpBomAsText(bs *BomStub, b *Bom, out io.Writer) { + fmt.Fprintln(out) + fmt.Fprintf(out, "%s (version %s, created %s)\n", bs.Name, b.Version, b.Created) + fmt.Fprintf(out, "Creator: %s\n", bs.Owner) + if bs.Description != "" { + fmt.Fprintf(out, "Description: %s\n", bs.Description) + } + fmt.Println() + // "by line item" + fmt.Fprintf(out, "tag\tqty\tmanufacturer\tmpn\t\tdescription\t\tcomment\n") + for _, li := range b.LineItems { + fmt.Fprintf(out, "%s\t%d\t%s\t%s\t\t%s\t\t%s\n", + li.Tag, + len(li.Elements), + li.Manufacturer, + li.Mpn, + li.Description, + li.Comment) + } + /* // "by circuit element" + fmt.Fprintf(out, "tag\tsymbol\tmanufacturer\tmpn\t\tdescription\t\tcomment\n") + for _, li := range b.LineItems { + for _, elm := range li.Elements { + fmt.Fprintf(out, "%s\t%s\t%s\t%s\t\t%s\t\t%s\n", + li.Tag, + elm, + li.Manufacturer, + li.Mpn, + li.Description, + li.Comment) + } + } + */ +} + +func DumpBomAsCSV(bs *BomStub, b *Bom, out io.Writer) { + dumper := csv.NewWriter(out) + defer dumper.Flush() + // "by line item" + dumper.Write([]string{"qty", + "symbols", + "manufacturer", + "mpn", + "description", + "comment"}) + for _, li := range b.LineItems { + dumper.Write([]string{ + fmt.Sprint(len(li.Elements)), + strings.Join(li.Elements, ","), + li.Manufacturer, + li.Mpn, + li.Description, + li.Comment}) + } +} + +func DumpBomAsJSON(bs *BomStub, b *Bom, out io.Writer) { + + obj := map[string]interface{}{ + "bom_meta": bs, + "bom": b, + } + + enc := json.NewEncoder(out) + if err := enc.Encode(&obj); err != nil { + log.Fatal(err) + } +} + +func DumpBomAsXML(bs *BomStub, b *Bom, out io.Writer) { + + /* + obj := map[string] interface{} { + "BomMeta": bs, + "Bom": b, + } + */ + + enc := xml.NewEncoder(out) + if err := enc.Encode(bs); err != nil { + log.Fatal(err) + } + if err := enc.Encode(b); err != nil { + log.Fatal(err) + } +} @@ -5,44 +5,55 @@ import ( ) type OfferPrice struct { - Currency string - MinQty uint32 - Price float32 + Currency string `json:"currency"` + MinQty uint32 `json:"min_qty"` + Price float32 `json:"price"` } type Offer struct { - Distributor, Sku, Url, Comment string - Prices []OfferPrice + Distributor string `json:"distributor_name"` + Sku string `json:"sku"` + Url string `json:"distributor_url"` + Comment string `json:"comment"` + Prices []OfferPrice `json:"prices"` } type LineItem struct { - Mfg, Mpn, Description, Comment, Tag string - Elements []string // TODO: add "circuit element" type - Offers []Offer + Manufacturer string `json:"manufacturer"` + Mpn string `json:"mpn"` + Description string `json:"description"` + Comment string `json:"comment"` + Tag string `json:"tag"` + // TODO: add "circuit element" type? + Elements []string `json:"elements"` + Offers []Offer `json:"offers"` } func (li *LineItem) Id() string { - return li.Mfg + "::" + li.Mpn + return li.Manufacturer + "::" + li.Mpn } // The main anchor of a BOM as a cohesive whole, with a name and permissions. // Multiple BOMs are associated with a single BomStub; the currently active one // is the 'head'. type BomStub struct { - Name string - Owner string - Description string - HeadVersion string - Homepage *Url - IsPublicView, IsPublicEdit bool + Name string `json:"name"` + Owner string `json:"owner_name"` + Description string `json:"description"` + HeadVersion string `json:"head_version"` + Homepage *Url `json:"homepage_url"` + IsPublicView bool `json:"is_publicview",omitempty` + IsPublicEdit bool `json:"is_publicedit",omitempty` } // An actual list of parts/elements. Intended to be immutable once persisted. type Bom struct { - Version string - Created time.Time // TODO: unix timestamp? - Progeny string // where did this BOM come from? - LineItems []LineItem + Version string `json:"version"` + // TODO: unix timestamp? + Created time.Time `json:"created_ts"` + // "where did this BOM come from?" + Progeny string `json:"progeny",omitifempty` + LineItems []LineItem `json:"line_items"` } func NewBom(version string) *Bom { @@ -51,7 +62,7 @@ func NewBom(version string) *Bom { func (b *Bom) GetLineItem(mfg, mpn string) *LineItem { for _, li := range b.LineItems { - if li.Mfg == mfg && li.Mpn == mpn { + if li.Manufacturer == mfg && li.Mpn == mpn { return &li } } @@ -59,7 +70,7 @@ func (b *Bom) GetLineItem(mfg, mpn string) *LineItem { } func (b *Bom) AddLineItem(li *LineItem) error { - if eli := b.GetLineItem(li.Mfg, li.Mpn); eli != nil { + if eli := b.GetLineItem(li.Manufacturer, li.Mpn); eli != nil { return Error("This BOM already had an identical LineItem") } b.LineItems = append(b.LineItems, *li) @@ -73,7 +84,7 @@ func makeTestBom() *Bom { o := Offer{Sku: "A123", Distributor: "Acme", Prices: []OfferPrice{op1, op2}} //o.AddOfferPrice(op1) //o.AddOfferPrice(op2) - li := LineItem{Mfg: "WidgetCo", + li := LineItem{Manufacturer: "WidgetCo", Mpn: "WIDG0001", Elements: []string{"W1", "W2"}, Offers: []Offer{o}} diff --git a/core_test.go b/core_test.go index 700052f..b89e23f 100644 --- a/core_test.go +++ b/core_test.go @@ -7,7 +7,6 @@ import ( "testing" ) - func TestNewBom(t *testing.T) { b := makeTestBom() if b == nil { @@ -4,7 +4,7 @@ import ( "encoding/json" "log" "os" - "path" + "path" ) var bomstore BomStore @@ -27,7 +27,7 @@ type JSONFileBomStore struct { func NewJSONFileBomStore(fpath string) (*JSONFileBomStore, error) { err := os.MkdirAll(fpath, os.ModePerm|os.ModeDir) if err != nil && !os.IsExist(err) { - return nil, err + return nil, err } return &JSONFileBomStore{Rootfpath: fpath}, nil } @@ -35,7 +35,7 @@ func NewJSONFileBomStore(fpath string) (*JSONFileBomStore, error) { func OpenJSONFileBomStore(fpath string) (*JSONFileBomStore, error) { _, err := os.Open(fpath) if err != nil && !os.IsExist(err) { - return nil, err + return nil, err } return &JSONFileBomStore{Rootfpath: fpath}, nil } @@ -49,16 +49,17 @@ func (jfbs *JSONFileBomStore) GetStub(user, name ShortName) (*BomStub, error) { return &bs, nil } -func (jfbs *JSONFileBomStore) GetHead(user, name ShortName) (*Bom, error) { +func (jfbs *JSONFileBomStore) GetHead(user, name ShortName) (*BomStub, *Bom, error) { bs, err := jfbs.GetStub(user, name) if err != nil { - return nil, err + return nil, nil, err } version := bs.HeadVersion if version == "" { log.Fatal("Tried to read undefined HEAD for " + string(user) + "/" + string(name)) } - return jfbs.GetBom(user, name, ShortName(version)) + b, err := jfbs.GetBom(user, name, ShortName(version)) + return bs, b, err } func (jfbs *JSONFileBomStore) GetBom(user, name, version ShortName) (*Bom, error) { @@ -71,73 +72,73 @@ func (jfbs *JSONFileBomStore) GetBom(user, name, version ShortName) (*Bom, error } func (jfbs *JSONFileBomStore) ListBoms(user ShortName) ([]BomStub, error) { - if user != "" { - return jfbs.listBomsForUser(user) - } - // else iterator over all users... - rootDir, err := os.Open(jfbs.Rootfpath) - if err != nil { - log.Fatal(err) - } - defer rootDir.Close() - bsList := []BomStub{} - dirInfo, err := rootDir.Readdir(0) - for _, node := range dirInfo { - if !node.IsDir() || !isShortName(node.Name()) { - continue - } - uList, err := jfbs.listBomsForUser(ShortName(node.Name())) - if err != nil { - log.Fatal(err) - } - bsList = append(bsList, uList...) - } - return bsList, nil + if user != "" { + return jfbs.listBomsForUser(user) + } + // else iterator over all users... + rootDir, err := os.Open(jfbs.Rootfpath) + if err != nil { + log.Fatal(err) + } + defer rootDir.Close() + bsList := []BomStub{} + dirInfo, err := rootDir.Readdir(0) + for _, node := range dirInfo { + if !node.IsDir() || !isShortName(node.Name()) { + continue + } + uList, err := jfbs.listBomsForUser(ShortName(node.Name())) + if err != nil { + log.Fatal(err) + } + bsList = append(bsList, uList...) + } + return bsList, nil } func (jfbs *JSONFileBomStore) listBomsForUser(user ShortName) ([]BomStub, error) { - bsList := []BomStub{} - uDirPath:= jfbs.Rootfpath + "/" + string(user) - uDir, err := os.Open(uDirPath) - if err != nil { - if e, ok := err.(*os.PathError); ok && e.Err.Error() == "no such file or directory" { - // XXX: should probably check for a specific syscall error? same below - return bsList, nil - } - return nil, err - } - defer uDir.Close() - dirContents , err := uDir.Readdir(0) - if err != nil { - return nil, err - } - for _, node := range dirContents { - if !node.IsDir() || !isShortName(node.Name()) { - continue - } - fpath := jfbs.Rootfpath + "/" + string(user) + "/" + node.Name() + "/_meta.json" - bs := BomStub{} - if err := readJsonBomStub(fpath, &bs); err != nil { - if e, ok := err.(*os.PathError); ok && e.Err.Error() == "no such file or directory" { - // no _meta.json in there - continue - } - return nil, err - } - bsList = append(bsList, bs) - } + bsList := []BomStub{} + uDirPath := jfbs.Rootfpath + "/" + string(user) + uDir, err := os.Open(uDirPath) + if err != nil { + if e, ok := err.(*os.PathError); ok && e.Err.Error() == "no such file or directory" { + // XXX: should probably check for a specific syscall error? same below + return bsList, nil + } + return nil, err + } + defer uDir.Close() + dirContents, err := uDir.Readdir(0) + if err != nil { + return nil, err + } + for _, node := range dirContents { + if !node.IsDir() || !isShortName(node.Name()) { + continue + } + fpath := jfbs.Rootfpath + "/" + string(user) + "/" + node.Name() + "/_meta.json" + bs := BomStub{} + if err := readJsonBomStub(fpath, &bs); err != nil { + if e, ok := err.(*os.PathError); ok && e.Err.Error() == "no such file or directory" { + // no _meta.json in there + continue + } + return nil, err + } + bsList = append(bsList, bs) + } return bsList, nil } func (jfbs *JSONFileBomStore) Persist(bs *BomStub, b *Bom, version ShortName) error { b_fpath := jfbs.Rootfpath + "/" + string(bs.Owner) + "/" + string(bs.Name) + "/" + string(version) + ".json" bs_fpath := jfbs.Rootfpath + "/" + string(bs.Owner) + "/" + string(bs.Name) + "/_meta.json" - if err := writeJsonBomStub(bs_fpath, bs); err != nil { - log.Fatal(err) - } - if err := writeJsonBom(b_fpath, b); err != nil { - log.Fatal(err) - } + if err := writeJsonBomStub(bs_fpath, bs); err != nil { + log.Fatal(err) + } + if err := writeJsonBom(b_fpath, b); err != nil { + log.Fatal(err) + } return nil } @@ -155,10 +156,10 @@ func readJsonBomStub(fpath string, bs *BomStub) error { } func writeJsonBomStub(fpath string, bs *BomStub) error { - err := os.MkdirAll(path.Dir(fpath), os.ModePerm|os.ModeDir) - if err != nil && !os.IsExist(err) { - return err - } + err := os.MkdirAll(path.Dir(fpath), os.ModePerm|os.ModeDir) + if err != nil && !os.IsExist(err) { + return err + } f, err := os.Create(fpath) if err != nil { return err |