-
Notifications
You must be signed in to change notification settings - Fork 68
feat: new formats for currencies (ISO-4217 alpha-3) and countries (ISO-3166 alpha-3 & alpha-2) #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package strfmt | ||
|
|
||
| import ( | ||
| "database/sql/driver" | ||
| "encoding/json" | ||
| "fmt" | ||
|
|
||
| "github.com/go-openapi/strfmt/internal/countries" | ||
| ) | ||
|
|
||
| // Country represents an ISO 3166 country alpha-3 or alpha-2 code as a string format. | ||
| // | ||
| // swagger:strfmt country. | ||
| type Country struct { | ||
| countries.Country | ||
|
|
||
| l int | ||
| } | ||
|
|
||
| const ( | ||
| alpha2Len = 2 | ||
| alpha3Len = 3 | ||
| ) | ||
|
|
||
| // IsCountry checks if a string is a valid ISO 3166 country format. | ||
| func IsCountry(str string) bool { | ||
| _, err := ParseCountry(str) | ||
|
|
||
| return err == nil | ||
| } | ||
|
|
||
| // ParseCountry parses a string that represents a valid [Country]. | ||
| func ParseCountry(str string) (Country, error) { | ||
| l := len(str) | ||
| switch l { | ||
| case alpha3Len: | ||
| c, ok := countries.CountriesISO3[str] | ||
| if !ok { | ||
| return Country{}, fmt.Errorf("unrecognized strfmt.Country %q: %w", str, ErrFormat) | ||
| } | ||
|
|
||
| return Country{Country: c, l: alpha3Len}, nil | ||
| case alpha2Len: | ||
| c, ok := countries.CountriesISO2[str] | ||
| if !ok { | ||
| return Country{}, fmt.Errorf("unrecognized strfmt.Country %q: %w", str, ErrFormat) | ||
| } | ||
|
|
||
| return Country{Country: c, l: alpha2Len}, nil | ||
| default: | ||
| return Country{}, fmt.Errorf("invalid length for strfmt.Country in: %q: %w", str, ErrFormat) | ||
| } | ||
| } | ||
|
|
||
| // MarshalText returns this instance into text. | ||
| func (u Country) MarshalText() ([]byte, error) { | ||
| return []byte(u.String()), nil | ||
| } | ||
|
|
||
| // UnmarshalText hydrates this instance from text. | ||
| func (u *Country) UnmarshalText(data []byte) error { // validation is performed later on | ||
| c, err := ParseCountry(string(data)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| *u = c | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Scan read a value from a database driver. | ||
| func (u *Country) Scan(raw any) error { | ||
| switch v := raw.(type) { | ||
| case []byte: | ||
| c, err := ParseCountry(string(v)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| *u = c | ||
| case string: | ||
| c, err := ParseCountry(v) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| *u = c | ||
| default: | ||
| return fmt.Errorf("cannot sql.Scan() strfmt.Country from: %#v: %w", v, ErrFormat) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Value converts a value to a database driver value. | ||
| func (u Country) Value() (driver.Value, error) { | ||
| return driver.Value(u.String()), nil | ||
| } | ||
|
|
||
| func (u Country) String() string { | ||
| switch u.l { | ||
| case alpha3Len: | ||
| return u.ISOAlpha3 | ||
| case alpha2Len: | ||
| return u.ISOAlpha2 | ||
| default: | ||
| return "" | ||
| } | ||
| } | ||
|
Comment on lines
+102
to
+111
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can see you use String everywhere (Marshal and others), which makes unserializaton and serialization stable But as a user, when you get a country with FRA or FR and send it to your lib, I would expect to get France
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the name "France" is resolved in the embedded type, but not used for wire-serialization
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the name "France" is resolved in the embedded type, but not used for wire-serialization
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so you have to use Country.Name
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but why String would return you passed to the lib. Maybe you should mention the lib is is made for being idempotent. It might be cleaer why FRA is returned when you pass FRA, either in string, JSON |
||
|
|
||
| // MarshalJSON returns the [Country] as JSON. | ||
| func (u Country) MarshalJSON() ([]byte, error) { | ||
| return json.Marshal(u.String()) | ||
| } | ||
|
|
||
| // UnmarshalJSON sets the [Country] from JSON. | ||
| func (u *Country) UnmarshalJSON(data []byte) error { | ||
| if string(data) == jsonNull { | ||
| return nil | ||
| } | ||
| var ustr string | ||
| if err := json.Unmarshal(data, &ustr); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| c, err := ParseCountry(ustr) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| *u = c | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // DeepCopyInto copies the receiver and writes its value into out. | ||
| func (u *Country) DeepCopyInto(out *Country) { | ||
| *out = *u | ||
| } | ||
|
|
||
| // DeepCopy copies the receiver into a new [Country]. | ||
| func (u *Country) DeepCopy() *Country { | ||
| if u == nil { | ||
| return nil | ||
| } | ||
|
|
||
| out := new(Country) | ||
| u.DeepCopyInto(out) | ||
|
|
||
| return out | ||
| } | ||
|
|
||
| // GobEncode implements the gob.GobEncoder interface. | ||
| func (u Country) GobEncode() ([]byte, error) { | ||
| return u.MarshalText() | ||
| } | ||
|
|
||
| // GobDecode implements the gob.GobDecoder interface. | ||
| func (u *Country) GobDecode(data []byte) error { | ||
| return u.UnmarshalText(data) | ||
| } | ||
|
|
||
| // MarshalBinary implements the encoding.[encoding.BinaryMarshaler] interface. | ||
| func (u Country) MarshalBinary() ([]byte, error) { | ||
| return u.MarshalText() | ||
| } | ||
|
|
||
| // UnmarshalBinary implements the encoding.[encoding.BinaryUnmarshaler] interface. | ||
| func (u *Country) UnmarshalBinary(data []byte) error { | ||
| return u.UnmarshalText(data) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package strfmt | ||
|
|
||
| import ( | ||
| "iter" | ||
| "slices" | ||
| "testing" | ||
|
|
||
| "github.com/go-openapi/testify/v2/require" | ||
| ) | ||
|
|
||
| func TestCountry(t *testing.T) { | ||
| for tc := range countryCases() { | ||
| t.Run(tc.Desc, func(t *testing.T) { | ||
| if tc.Valid { | ||
| require.TrueT(t, IsCountry(tc.Input)) | ||
|
|
||
| cur, err := ParseCountry(tc.Input) | ||
| require.NoError(t, err) | ||
|
|
||
| b, err := cur.MarshalText() | ||
| require.NoError(t, err) | ||
| require.NotEmpty(t, b) | ||
|
|
||
| var other Country | ||
| require.NoError(t, other.UnmarshalText(b)) | ||
|
|
||
| require.EqualT(t, cur.String(), other.String()) | ||
|
|
||
| return | ||
| } | ||
|
|
||
| require.FalseT(t, IsCountry(tc.Input)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| type countryCase struct { | ||
| Desc string | ||
| Input string | ||
| Valid bool | ||
| } | ||
|
|
||
| func countryCases() iter.Seq[countryCase] { | ||
| return slices.Values([]countryCase{ | ||
| {Desc: "USA", Input: "USA", Valid: true}, | ||
| {Desc: "FR-alpha-2", Input: "FR", Valid: true}, | ||
| {Desc: "GBR-alpha-3", Input: "GBR", Valid: true}, | ||
| {Desc: "unknown-alpha-2", Input: "FF", Valid: false}, | ||
| {Desc: "unknown-alpha-3", Input: "XYZ", Valid: false}, | ||
| {Desc: "invalid (empty)", Input: "", Valid: false}, | ||
| {Desc: "invalid (1)", Input: "X", Valid: false}, | ||
| {Desc: "invalid (4)", Input: "ABCD", Valid: false}, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could have updated to use 2026, even if I know copyright date ranges are usually misunderstood
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we've been there already, don't recall it?