aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbnewbold <bnewbold@robocracy.org>2012-04-10 20:58:13 -0400
committerbnewbold <bnewbold@robocracy.org>2012-04-11 10:58:08 -0400
commit78b207a40436d0c15a2b806171914d802cd20661 (patch)
tree730d02b32e5d54b2512a319d7ced34ad8ce3aacf
parent3c7a4451e62d27bbe9dc8eb2c16e2ff5607d1b04 (diff)
downloadbommom-78b207a40436d0c15a2b806171914d802cd20661.tar.gz
bommom-78b207a40436d0c15a2b806171914d802cd20661.zip
tests passing
-rw-r--r--README21
-rw-r--r--core.go59
-rw-r--r--core_test.go41
-rw-r--r--store.go109
-rw-r--r--util.go23
-rw-r--r--util_test.go32
6 files changed, 267 insertions, 18 deletions
diff --git a/README b/README
index 275776c..a1ec91f 100644
--- a/README
+++ b/README
@@ -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)
+
diff --git a/core.go b/core.go
index 4472f07..3b756d8 100644
--- a/core.go
+++ b/core.go
@@ -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())
+ }
+}
diff --git a/store.go b/store.go
index 3fb279f..11b2b5c 100644
--- a/store.go
+++ b/store.go
@@ -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
}
diff --git a/util.go b/util.go
index 6b5049e..c8ed617 100644
--- a/util.go
+++ b/util.go
@@ -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)
+ }
+ }
+}