aboutsummaryrefslogtreecommitdiff
path: root/refined
diff options
context:
space:
mode:
Diffstat (limited to 'refined')
-rw-r--r--refined/condition.go20
-rw-r--r--refined/value.go61
-rw-r--r--refined/value_test.go61
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
+}