aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--auth.go8
-rw-r--r--bommom.go271
-rw-r--r--core.go55
-rw-r--r--core_test.go1
-rw-r--r--store.go137
5 files changed, 322 insertions, 150 deletions
diff --git a/auth.go b/auth.go
index 0bbed40..d8e881e 100644
--- a/auth.go
+++ b/auth.go
@@ -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
}
diff --git a/bommom.go b/bommom.go
index 4b6e39d..f7ea966 100644
--- a/bommom.go
+++ b/bommom.go
@@ -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)
+ }
+}
diff --git a/core.go b/core.go
index 5d76f71..087a3f4 100644
--- a/core.go
+++ b/core.go
@@ -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 {
diff --git a/store.go b/store.go
index 105890c..182f97d 100644
--- a/store.go
+++ b/store.go
@@ -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