diff options
Diffstat (limited to 'refined')
| -rw-r--r-- | refined/condition.go | 20 | ||||
| -rw-r--r-- | refined/value.go | 61 | ||||
| -rw-r--r-- | refined/value_test.go | 61 |
3 files changed, 142 insertions, 0 deletions
diff --git a/refined/condition.go b/refined/condition.go new file mode 100644 index 0000000..5b43554 --- /dev/null +++ b/refined/condition.go @@ -0,0 +1,20 @@ +/******************************************************************************* +* Copyright 2025 Stefan Majewsky <majewsky@gmx.net> +* SPDX-License-Identifier: Apache-2.0 +* Refer to the file "LICENSE" for details. +*******************************************************************************/ + +package refined + +import ( + "fmt" + "regexp" +) + +// Building block for writing MatchesValue() implementations. +func RegexpMatch(rx *regexp.Regexp, value string) error { + if !rx.MatchString(value) { + return fmt.Errorf("provided value %q does not match expected pattern %q", value, rx.String()) + } + return nil +} diff --git a/refined/value.go b/refined/value.go new file mode 100644 index 0000000..0d480ba --- /dev/null +++ b/refined/value.go @@ -0,0 +1,61 @@ +/******************************************************************************* +* Copyright 2025 Stefan Majewsky <majewsky@gmx.net> +* SPDX-License-Identifier: Apache-2.0 +* Refer to the file "LICENSE" for details. +*******************************************************************************/ + +package refined + +import ( + "encoding/json" + + //nolint:staticcheck // this dot import is fine (ST1001) + . "github.com/majewsky/gg/option" +) + +// NOTE: The zero value is illegal and will panic on use. +type Value[V any, C Condition[V]] struct { + value Option[V] +} + +type Condition[V any] interface { + MatchesValue(V) error +} + +func NewValue[V any, C Condition[V]](value V) (Value[V, C], error) { + var cond C + err := cond.MatchesValue(value) + if err == nil { + return Value[V, C]{value: Some(value)}, nil + } else { + return Value[V, C]{}, err + } +} + +func LiteralValue[V any, C Condition[V]](value V) Value[V, C] { + var cond C + err := cond.MatchesValue(value) + if err == nil { + return Value[V, C]{value: Some(value)} + } else { + panic(err.Error()) + } +} + +func (v Value[V, C]) Raw() V { + return v.value.UnwrapOrPanic("illegal use of zero-valued instance of refined.Value") +} + +func (v *Value[V, C]) UnmarshalJSON(buf []byte) error { + var value V + err := json.Unmarshal(buf, &value) + if err != nil { + return err + } + *v, err = NewValue[V, C](value) + return err +} + +func (v Value[V, C]) MarshalJSON() ([]byte, error) { + return json.Marshal(v.Raw()) +} diff --git a/refined/value_test.go b/refined/value_test.go new file mode 100644 index 0000000..0303071 --- /dev/null +++ b/refined/value_test.go @@ -0,0 +1,61 @@ +/******************************************************************************* +* Copyright 2025 Stefan Majewsky <majewsky@gmx.net> +* SPDX-License-Identifier: Apache-2.0 +* refined.Refer to the file "LICENSE" for details. +*******************************************************************************/ + +package refined_test + +import ( + "encoding/json" + "regexp" + "testing" + + . "github.com/majewsky/gg/internal/test" + "github.com/majewsky/gg/refined" +) + +var accountNameRx = regexp.MustCompile(`^[a-z_][a-z0-9_]*$`) + +// Full demonstration of a refinement type for the test. +type AccountName = refined.Value[string, accountNameCondition] + +type accountNameCondition struct{} + +func (accountNameCondition) MatchesValue(value string) error { + return refined.RegexpMatch(accountNameRx, value) +} + +// Demonstration of a struct containing a refinement type. +type AccountData struct { + Name AccountName +} + +func TestAccountName(t *testing.T) { + buf1 := []byte(`{"Name":"foo"}`) + var d1 AccountData + err := json.Unmarshal(buf1, &d1) + AssertEqual(t, err, error(nil)) + AssertEqual(t, d1.Name.Raw(), "foo") + + // TODO: fails because we need specialized unmarshaling logic on type AccountData + buf2 := []byte(`{}`) + var d2 AccountData + err = json.Unmarshal(buf2, &d2) + AssertEqual(t, err.Error(), "foo") +} + +func TestRefinedMapKeys(t *testing.T) { + var ( + // TODO: yuck + foo = AccountName(refined.LiteralValue[string, accountNameCondition]("foo")) + bar = AccountName(refined.LiteralValue[string, accountNameCondition]("bar")) + ) + m := map[AccountName]int{ + foo: 3, + bar: 1, + } + // TODO: AccountName is not an ordered type; we might need stuff like slices.Sorted() in package refined + // AssertEqual(slices.Sorted(maps.Keys(m)), []AccountName{bar, foo}) + _ = m +} |
