-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…#99) * GH-44 implement driver.Valuer and sql.Scanner for money.Currency This allows any Golang ORM which supports the sql.Scanner to serialize (via sql.Driver) and deserialize (via sql.Scanner) a money.Currency instance. money.Amount is now a type alias to int64 which is already supported by sql.Scanner as one of the core built-in data types * GH-44 implement driver.Value and sql.Scanner for money.Money Money's Value() function enables compatible sql drivers to serialize a money.Money instance to a single comma-delimited string value of "amount,currency_code" Money's Scan() function assumes that it receives a single column where the src value is a comma- delimited string in the format "amount,currency_code" While the storage format is up to the client when the amount and currency are stored separately a compatible scanner value can be created in SQLite like this: SELECT amount || "," || currency as 'amount' It is left to the client to decide to use Money's Valuer implementation with a db annotation on a property of type Money or else to store the Amount and Currency values as two separate columns and return them as a single joined string field. * GH-44 fix an edge case strings.Split(src,,) will return a slice with length 2 even if one of the strings is empty * fix: money.value tests * refactor out the currency separator and make it customizable clients can set money.DBMoneyValueSeparator to determine which separator (e.g. "," "|" ";" ":" "AS" etc) to use when creating a single driver.Value object to represent a money.Money instance as a single string database field. this allows the money package to support string values such as 10@USD 20;CAD 30|IRD 40 in GBP etc --------- Co-authored-by: Raymond <[email protected]>
- Loading branch information
1 parent
75eda0b
commit 89bfd6b
Showing
3 changed files
with
246 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package money | ||
|
||
import ( | ||
"database/sql/driver" | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
var ( | ||
// DBMoneyValueSeparator is used to join together the Amount and Currency components of money.Money instances | ||
// allowing them to be stored as strings (via the driver.Valuer interface) and unmarshalled as strings (via | ||
// the sql.Scanner interface); set this value to use a different separator. | ||
DBMoneyValueSeparator = DefaultDBMoneyValueSeparator | ||
) | ||
|
||
const ( | ||
// DefaultDBMoneyValueSeparator is the default value for DBMoneyValueSeparator; can be used to reset the | ||
// active separator value | ||
DefaultDBMoneyValueSeparator = "|" | ||
) | ||
|
||
// Value implements driver.Valuer to serialise a Money instance into a delimited string using the DBMoneyValueSeparator | ||
// for example: "amount|currency_code" | ||
func (m *Money) Value() (driver.Value, error) { | ||
return fmt.Sprintf("%d%s%s", m.amount, DBMoneyValueSeparator, m.Currency().Code), nil | ||
} | ||
|
||
// Scan implements sql.Scanner to deserialize a Money instance from a DBMoneyValueSeparator-separated string | ||
// for example: "amount|currency_code" | ||
func (m *Money) Scan(src interface{}) error { | ||
var amount Amount | ||
currency := &Currency{} | ||
|
||
// let's support string and int64 | ||
switch src.(type) { | ||
case string: | ||
parts := strings.Split(src.(string), DBMoneyValueSeparator) | ||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" { | ||
return fmt.Errorf("%#v is not valid to scan into Money; update your query to return a money.DBMoneyValueSeparator-separated pair of \"amount%scurrency_code\"", src.(string), DBMoneyValueSeparator) | ||
} | ||
|
||
if a, err := strconv.ParseInt(parts[0], 10, 64); err == nil { | ||
amount = a | ||
} else { | ||
return fmt.Errorf("scanning %#v into an Amount: %v", parts[0], err) | ||
} | ||
|
||
if err := currency.Scan(parts[1]); err != nil { | ||
return fmt.Errorf("scanning %#v into a Currency: %v", parts[1], err) | ||
} | ||
default: | ||
return fmt.Errorf("don't know how to scan %T into Money; update your query to return a money.DBMoneyValueSeparator-separated pair of \"amount%scurrency_code\"", src, DBMoneyValueSeparator) | ||
} | ||
|
||
// allocate new Money with the scanned amount and currency | ||
*m = Money{ | ||
amount: amount, | ||
currency: currency, | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Value implements driver.Valuer to serialize a Currency code into a string for saving to a database | ||
func (c Currency) Value() (driver.Value, error) { | ||
return c.Code, nil | ||
} | ||
|
||
// Scan implements sql.Scanner to deserialize a Currency from a string value read from a database | ||
func (c *Currency) Scan(src interface{}) error { | ||
var val *Currency | ||
// let's support string only | ||
switch src.(type) { | ||
case string: | ||
val = GetCurrency(src.(string)) | ||
default: | ||
return fmt.Errorf("%T is not a supported type for a Currency (store the Currency.Code value as a string only)", src) | ||
} | ||
|
||
if val == nil { | ||
return fmt.Errorf("GetCurrency(%#v) returned nil", src) | ||
} | ||
|
||
// copy the value | ||
*c = *val | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package money | ||
|
||
import ( | ||
"database/sql/driver" | ||
"fmt" | ||
"reflect" | ||
"testing" | ||
) | ||
|
||
func TestMoney_Value(t *testing.T) { | ||
tests := []struct { | ||
have *Money | ||
separator string | ||
want string | ||
wantErr bool | ||
}{ | ||
{ | ||
have: New(10, CAD), | ||
separator: "|", | ||
want: "10|CAD", | ||
}, | ||
{ | ||
have: New(-10, USD), | ||
separator: "+-+", | ||
want: "-10+-+USD", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(fmt.Sprintf("%#v", tt.have), func(t *testing.T) { | ||
want := driver.Value(tt.want) | ||
DBMoneyValueSeparator = tt.separator | ||
got, err := tt.have.Value() | ||
if err != nil { | ||
t.Errorf("Value() error = %v", err) | ||
return | ||
} | ||
if !reflect.DeepEqual(got, want) { | ||
t.Errorf("Value() got = %v, want %v", got, want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestMoney_Scan(t *testing.T) { | ||
tests := []struct { | ||
src interface{} | ||
separator string | ||
want *Money | ||
wantErr bool | ||
}{ | ||
{ | ||
src: "10|CAD", | ||
want: New(10, CAD), | ||
}, | ||
{ | ||
src: "20|USD", | ||
want: New(20, USD), | ||
}, | ||
{ | ||
src: "30000,IDR", | ||
separator: ",", | ||
want: New(30000, IDR), | ||
}, | ||
{ | ||
src: "10|", | ||
wantErr: true, | ||
}, | ||
{ | ||
src: "|SAR", | ||
wantErr: true, | ||
}, | ||
{ | ||
src: "10", | ||
wantErr: true, | ||
}, | ||
{ | ||
src: "USD", | ||
wantErr: true, | ||
}, | ||
{ | ||
src: "USD|10", | ||
wantErr: true, | ||
}, | ||
{ | ||
src: "", | ||
wantErr: true, | ||
}, | ||
{ | ||
src: "a|b|c", | ||
wantErr: true, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(fmt.Sprintf("%#v", tt.src), func(t *testing.T) { | ||
if tt.separator != "" { | ||
DBMoneyValueSeparator = tt.separator | ||
} else { | ||
DBMoneyValueSeparator = DefaultDBMoneyValueSeparator | ||
} | ||
got := &Money{} | ||
if err := got.Scan(tt.src); (err != nil) != tt.wantErr { | ||
t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
if tt.wantErr { | ||
return | ||
} | ||
if got == nil { | ||
t.Errorf("money.Scan() result was <nil>") | ||
return | ||
} | ||
eq, err := tt.want.Equals(got) | ||
if err != nil { | ||
t.Errorf(err.Error()) | ||
} | ||
if !eq { | ||
t.Errorf("Value() got = %s %s, want %s %s", got.Display(), got.Currency().Code, tt.want.Display(), tt.want.Currency().Code) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestCurrency_Value(t *testing.T) { | ||
for code, cc := range currencies { | ||
t.Run(code, func(t *testing.T) { | ||
want := driver.Value(code) | ||
|
||
got, err := cc.Value() | ||
if err != nil { | ||
t.Errorf("Value() error = %v", err) | ||
return | ||
} | ||
if !reflect.DeepEqual(got, want) { | ||
t.Errorf("Value() got = %v, want %v", got, want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestCurrency_Scan(t *testing.T) { | ||
for code, want := range currencies { | ||
t.Run(code, func(t *testing.T) { | ||
src := interface{}(code) | ||
|
||
got := &Currency{} | ||
err := got.Scan(src) | ||
if err != nil { | ||
t.Errorf("Scan() error = %v", err) | ||
} | ||
if !reflect.DeepEqual(got, want) { | ||
t.Errorf("Scan() got %#v, want %#v", got, want) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters