Skip to content

Commit

Permalink
Merge pull request #436 from iov-one/human_readable_coin_json
Browse files Browse the repository at this point in the history
Support human readable JSON format for Coin.
  • Loading branch information
husio authored Mar 25, 2019
2 parents 4c2f122 + 734be10 commit b33f16f
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 4 deletions.
7 changes: 3 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,9 @@ cover:
cat coverage/*.out > coverage/coverage.txt

deps:
#rm -rf vendor/
ifndef $(shell command -v dep help > /dev/null)
go get github.com/golang/dep/cmd/dep
endif
ifndef $(shell command -v dep help > /dev/null)
go get github.com/golang/dep/cmd/dep
endif
dep ensure -vendor-only

lint:
Expand Down
72 changes: 72 additions & 0 deletions coin/coin.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package coin

import (
"encoding/json"
fmt "fmt"
"regexp"
"strconv"

"github.com/iov-one/weave/errors"
)
Expand Down Expand Up @@ -303,3 +306,72 @@ func (c Coin) normalize() (Coin, error) {
}
return c, nil
}

func (c *Coin) UnmarshalJSON(raw []byte) error {
// Prioritize human readable format that is a string in format
// "<whole>[.<fractional>] <ticker>"
var human string
if err := json.Unmarshal(raw, &human); err == nil {
whole, fract, ticker, err := parseHumanFormat(human)
if err == nil {
c.Whole = whole
c.Fractional = fract
c.Ticker = ticker
}
return err
}

// Fallback into the default unmarhaling. Because UnmarshalJSON method
// is provided, we can no longer use Coin type for this.
var coin struct {
Whole int64
Fractional int64
Ticker string
}
if err := json.Unmarshal(raw, &coin); err != nil {
return err
}
c.Whole = coin.Whole
c.Fractional = coin.Fractional
c.Ticker = coin.Ticker
return nil
}

// parseHumanFormat parse a human readable coin represenation. Accepted format
// is a string:
// "<whole>[.<fractional>] <ticker>"
func parseHumanFormat(h string) (int64, int64, string, error) {
results := humanCoinFormatRx.FindAllStringSubmatch(h, -1)
if len(results) != 1 {
return 0, 0, "", fmt.Errorf("invalid format")
}

result := results[0][1:]

whole, err := strconv.ParseInt(result[1], 10, 64)
if err != nil {
return 0, 0, "", fmt.Errorf("invalid whole value: %s", err)
}

var fract int64
if result[2] != "" {
val, err := strconv.ParseFloat(result[2], 64)
if err != nil {
return 0, 0, "", fmt.Errorf("invalid fractional value: %s", err)
}
// Max float64 value is around 1.7e+308 so I do not think we
// should bother with the overflow issue.
fract = int64(val * float64(FracUnit))
}

ticker := result[3]

if result[0] == "-" {
whole = -whole
fract = -fract
}

return whole, fract, ticker, nil
}

var humanCoinFormatRx = regexp.MustCompile(`^(\-?)\s*(\d+)(\.\d+)?\s*([A-Z]{3,4})$`)
110 changes: 110 additions & 0 deletions coin/coin_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package coin

import (
"encoding/json"
"fmt"
"math"
"testing"
Expand Down Expand Up @@ -439,3 +440,112 @@ func TestCoinMultiply(t *testing.T) {
})
}
}

func TestCoinDeserialization(t *testing.T) {
cases := map[string]struct {
serialized string
wantErr bool
wantCoin Coin
}{
"old format coin, that maps to fields directly": {
serialized: `{"whole": 1, "fractional": 2, "ticker": "IOV"}`,
wantCoin: NewCoin(1, 2, "IOV"),
},
"old format coin, only whole": {
serialized: `{"whole": 1}`,
wantCoin: NewCoin(1, 0, ""),
},
"old format coin, only fractional": {
serialized: `{"fractional": 1}`,
wantCoin: NewCoin(0, 1, ""),
},
"old format coin, only ticker": {
serialized: `{"ticker": "IOV"}`,
wantCoin: NewCoin(0, 0, "IOV"),
},
"old format empty coin, that maps to fields directly": {
serialized: `{}`,
wantCoin: NewCoin(0, 0, ""),
},
"human readable format, whole without fractional": {
serialized: `"1IOV"`,
wantCoin: NewCoin(1, 0, "IOV"),
},
"human readable format, whole without fractional, ticker space separated": {
serialized: `"1 IOV"`,
wantCoin: NewCoin(1, 0, "IOV"),
},
"human readable format, whole and fractional": {
serialized: `"1.000000002IOV"`,
wantCoin: NewCoin(1, 2, "IOV"),
},
"human readable format, whole and fractional, ticker space separated": {
serialized: `"1.000000002 IOV"`,
wantCoin: NewCoin(1, 2, "IOV"),
},
"human readable format, zero whole and fractional": {
serialized: `"0.000000002IOV"`,
wantCoin: NewCoin(0, 2, "IOV"),
},
"human readable format, missing whole": {
serialized: `".0000000002IOV"`,
wantErr: true,
},
"human readable format, only whole": {
serialized: `"1"`,
wantErr: true,
},
"human readable format, missing ticker": {
serialized: `"1.0000000002"`,
wantErr: true,
},
"human readable format, only ticker": {
serialized: `"IOV"`,
wantErr: true,
},
"human readable format, ticker too short": {
serialized: `"1 AB"`,
wantErr: true,
},
"human readable format, ticker too long": {
serialized: `"1 ABCDE"`,
wantErr: true,
},
"human readable format, negative value": {
serialized: `"-4.000000002 IOV"`,
wantCoin: NewCoin(4, 2, "IOV").Negative(),
},
"human readable format, negative value, no whole": {
serialized: `"-0.000000002 IOV"`,
wantCoin: NewCoin(0, 2, "IOV").Negative(),
},
"human readable format, negative zero": {
serialized: `"-0 IOV"`,
wantCoin: NewCoin(0, 0, "IOV").Negative(),
},
"human readable format, zero": {
serialized: `"0 IOV"`,
wantCoin: NewCoin(0, 0, "IOV"),
},
"human readable format, double negative": {
serialized: `"--1 IOV"`,
wantErr: true,
},
}

for testName, tc := range cases {
t.Run(testName, func(t *testing.T) {
var got Coin
if err := json.Unmarshal([]byte(tc.serialized), &got); err != nil {
if !tc.wantErr {
t.Fatalf("cannot unmarshal: %s", err)
}
return
}

if !tc.wantCoin.Equals(got) {
t.Fatalf("unexpected coin result: %#v", got)
}
})
}
}

0 comments on commit b33f16f

Please sign in to comment.