diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2025-04-02 18:09:46 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2025-04-02 18:09:46 +0200 |
| commit | eab38629013e34b4490be4d665142a1357c97156 (patch) | |
| tree | 57fab149aa16d438032cae5fa7a89c0b201b92ae | |
| parent | 23862346411fb921e1a45f25011c3db601f8591d (diff) | |
| download | go-gg-refinement-types-2.tar.gz | |
demonstration of refinement types without self-referential typesrefinement-types-2
The big "yuck" about this can be seen in the LiteralValue invocations in
the test code. Calling NewValue or LiteralValue is extremely convoluted
and would likely need to be wrapped through additional boilerplate at
the concrete type declaration site.
| -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 +} |
