diff options
-rw-r--r-- | README | 21 | ||||
-rw-r--r-- | core.go | 59 | ||||
-rw-r--r-- | core_test.go | 41 | ||||
-rw-r--r-- | store.go | 109 | ||||
-rw-r--r-- | util.go | 23 | ||||
-rw-r--r-- | util_test.go | 32 |
6 files changed, 267 insertions, 18 deletions
@@ -12,3 +12,24 @@ BomMom: A web publishing/wiki system for electronics Bill of Materials (BOM) A work in progress as of early April 2012. +### Instructions + +Install golang compiler and run `go build` in this directory, then run the +`bommom` command to list available commands and options. + +### Intended Initial Features + + - SQL-backed datastore for BOMs and web authentication + - file-backed datastore for BOMs + - import/export to .csv and JSON + +### Potential Later Features + + - auto-submit orders to major distributors + - current inventory tracking + - per-part statistics (eg, most popular parts) + - REST API + - git-backed BomStore + - git post-commit hooks and/or github integration + - plugins for CAD software (Eagle, KiCad, etc) + @@ -1,30 +1,67 @@ package main +import ( + "time" +) + +type OfferPrice struct { + Currency string + MinQty uint32 + Price float32 +} + type Offer struct { + Distributor, Sku, Url, Comment string + Prices []OfferPrice } type LineItem struct { + Mfg, Mpn, Description, Comment, Tag string + Elements []string // TODO: add "circuit element" type + Offers []Offer } -type Element struct { +func (li *LineItem) Id() string { + return li.Mfg + "::" + 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 *ShortName - owner string - description string - homepage *Url - isPublicView, isPublicEdit bool + Name string + Owner string + Description string + HeadVersion string + Homepage *Url + IsPublicView, IsPublicEdit bool } // An actual list of parts/elements. Intended to be immutable once persisted. type Bom struct { - version *ShortName - date uint64 // TODO: unix timestamp? - progeny string // where did this BOM come from? - elements []Element - lineitems []LineItem + Version string + Created time.Time // TODO: unix timestamp? + Progeny string // where did this BOM come from? + LineItems []LineItem +} + +func NewBom(version string) *Bom { + return &Bom{Version: version, Created: time.Now()} +} + +func (b *Bom) GetLineItem(mfg, mpn string) *LineItem { + for _, li := range b.LineItems { + if li.Mfg == mfg && li.Mpn == mpn { + return &li + } + } + return nil +} + +func (b *Bom) AddLineItem(li *LineItem) error { + if eli := b.GetLineItem(li.Mfg, li.Mpn); eli != nil { + return Error("This BOM already had an identical LineItem") + } + b.LineItems = append(b.LineItems, *li) + return nil } diff --git a/core_test.go b/core_test.go new file mode 100644 index 0000000..7e6ae2d --- /dev/null +++ b/core_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "encoding/json" + //"fmt" + "os" + "testing" +) + +func makeTestBom() *Bom { + op1 := OfferPrice{Currency: "usd", Price: 1.0, MinQty: 1} + op2 := OfferPrice{Currency: "usd", Price: 0.8, MinQty: 100} + o := Offer{Sku: "A123", Distributor: "Acme", Prices: []OfferPrice{op1, op2}} + //o.AddOfferPrice(op1) + //o.AddOfferPrice(op2) + li := LineItem{Mfg: "WidgetCo", + Mpn: "WIDG0001", + Elements: []string{"W1", "W2"}, + Offers: []Offer{o}} + //li.AddOffer(o) + b := NewBom("test01") + b.AddLineItem(&li) + return b +} + +func TestNewBom(t *testing.T) { + b := makeTestBom() + if b == nil { + t.Errorf("Something went wrong") + } +} + +func TestBomJSONDump(t *testing.T) { + + b := makeTestBom() + enc := json.NewEncoder(os.Stdout) + + if err := enc.Encode(b); err != nil { + t.Errorf("Error encoding: " + err.Error()) + } +} @@ -1,5 +1,11 @@ package main +import ( + "encoding/json" + "log" + "os" +) + var bomstore BomStore // TODO: who owns returned BOMs? Caller? need "free" methods? @@ -7,16 +13,105 @@ type BomStore interface { GetStub(user, name ShortName) (*BomStub, error) GetHead(user, name ShortName) (*Bom, error) GetBom(user, name, version ShortName) (*Bom, error) - Persist(bom *Bom) error + Persist(bs *BomStub, b *Bom, version ShortName) error } -/* -// Dummy BomStore backed by hashtable in memory, for testing and demo purposes -type MemoryBomStore map[string] Bom -*/ - // Basic BomStore backend using a directory structure of JSON files saved to // disk. type JSONFileBomStore struct { - rootPath string + RootPath string +} + +func NewJSONFileBomStore(path string) *JSONFileBomStore { + err := os.MkdirAll(path, os.ModePerm|os.ModeDir) + if err != nil && !os.IsExist(err) { + log.Fatal(err) + } + return &JSONFileBomStore{RootPath: path} +} + +func (jfbs *JSONFileBomStore) GetStub(user, name ShortName) (*BomStub, error) { + path := jfbs.RootPath + "/" + string(user) + "/" + string(name) + "/meta.json" + bs := BomStub{} + if err := readJsonBomStub(path, &bs); err != nil { + return nil, err + } + return &bs, nil +} + +func (jfbs *JSONFileBomStore) GetHead(user, name ShortName) (*Bom, error) { + bs, err := jfbs.GetStub(user, name) + if err != nil { + return 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)) +} + +func (jfbs *JSONFileBomStore) GetBom(user, name, version ShortName) (*Bom, error) { + path := jfbs.RootPath + "/" + string(user) + "/" + string(name) + "/" + string(version) + ".json" + b := Bom{} + if err := readJsonBom(path, &b); err != nil { + return nil, err + } + return &b, nil +} + +func (jfbs *JSONFileBomStore) Persist(bs *BomStub, b *Bom, version ShortName) error { + return nil +} + +func readJsonBomStub(path string, bs *BomStub) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + dec := json.NewDecoder(f) + if err = dec.Decode(&bs); err != nil { + return err + } + return nil +} + +func writeJsonBomStub(path string, bs *BomStub) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + enc := json.NewEncoder(f) + if err = enc.Encode(&bs); err != nil { + return err + } + return nil +} + +func readJsonBom(path string, b *Bom) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + dec := json.NewDecoder(f) + if err = dec.Decode(&b); err != nil { + return err + } + return nil +} + +func writeJsonBom(path string, b *Bom) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + enc := json.NewEncoder(f) + if err = enc.Encode(&b); err != nil { + return err + } + return nil } @@ -1,5 +1,12 @@ package main +// Minimal Error type... is there a better way? +type Error string + +func (e Error) Error() string { + return string(e) +} + type EmailAddress string type Password string type Url string @@ -7,3 +14,19 @@ type Url string // "Slug" string with limited ASCII character set, good for URLs. // Lowercase alphanumeric plus '_' allowed. type ShortName string + +func isShortName(s string) bool { + for i, r := range s { + switch { + case '0' <= r && '9' >= r && i > 0: + continue + case 'a' <= r && 'z' >= r: + continue + case r == '_' && i > 0: + continue + default: + return false + } + } + return true +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..7a87bde --- /dev/null +++ b/util_test.go @@ -0,0 +1,32 @@ +package main + +import "testing" + +var yesShort = []string{ + "asdf", + "as12df", + "as_df", +} + +var noShort = []string{ + "(!&#$(&@!#", + "_asdf", + "as df", + "ASDF", + "AS_DF", + "2o45", + "as.12df", +} + +func TestIsShortName(t *testing.T) { + for _, y := range yesShort { + if !isShortName(y) { + t.Errorf("Is short: " + y) + } + } + for _, n := range noShort { + if isShortName(n) { + t.Errorf("Is not short: " + n) + } + } +} |