Skip to content

Commit d092b48

Browse files
new schemes
1 parent 4b794c0 commit d092b48

File tree

3 files changed

+346
-53
lines changed

3 files changed

+346
-53
lines changed

scheme/scheme.go

Lines changed: 216 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66
"fmt"
77
"math"
8+
"net/mail"
9+
"net/url"
810
"regexp"
911
"strings"
1012
"time"
@@ -28,6 +30,8 @@ const (
2830
ErrStringSuffix // suffix check failed
2931
ErrStringPattern // regex/pattern check failed
3032
ErrStringMatch // exact match failed
33+
ErrStringEmail // email format validation failed
34+
ErrStringURL // URL/URI format validation failed
3135

3236
// Numeric validation codes
3337
ErrOutOfRange // integer value out of allowed range
@@ -57,6 +61,10 @@ func (e ErrorCode) String() string {
5761
return "ErrStringPattern"
5862
case ErrStringMatch:
5963
return "ErrStringMatch"
64+
case ErrStringEmail:
65+
return "ErrStringEmail"
66+
case ErrStringURL:
67+
return "ErrStringURL"
6068
case ErrOutOfRange:
6169
return "ErrOutOfRange"
6270
case ErrDateOutOfRange:
@@ -155,6 +163,8 @@ const (
155163
SchemeNamedChainName = "SchemeNamedChain"
156164
SchemeMapUnorderedName = "SchemeMapUnordered"
157165
SchemeMultiCheckNamesSchemeNamed = "SchemeMultiCheckNamesSchemeNamed"
166+
SchemeDateName = "SchemeDate"
167+
SchemeEnumNamedListName = "SchemeEnumNamedList"
158168
ChainName = "SchemeChain"
159169

160170
TupleSchemeName = "TupleScheme"
@@ -523,8 +533,8 @@ type Nullable interface {
523533
IsNullable() bool
524534
}
525535

526-
func (s SchemeString) IsNullable() bool { return s.Width <= 0 }
527-
func (s SchemeBytes) IsNullable() bool { return s.Width <= 0 }
536+
func (s SchemeString) IsNullable() bool { return s.Width < 0 }
537+
func (s SchemeBytes) IsNullable() bool { return s.Width < 0 }
528538
func (s SchemeMap) IsNullable() bool { return s.Width <= 0 }
529539

530540
// Primitives
@@ -782,7 +792,7 @@ var (
782792
SNullInt64 Scheme = SchemeInt64{Nullable: true}
783793
SNullFloat32 Scheme = SchemeFloat32{Nullable: true}
784794
SNullFloat64 Scheme = SchemeFloat64{Nullable: true}
785-
SString SchemeString = SchemeString{Width: -1}
795+
SString SchemeString = SchemeString{Width: 0}
786796
SAny = SchemeAny{}
787797
)
788798

@@ -932,11 +942,8 @@ func precheck(errorName string, pos int, seq *access.SeqGetAccess, tag types.Typ
932942
return 0, NewSchemeError(ErrConstraintViolated, errorName, "", pos, ErrTypeMisMatch)
933943
}
934944

935-
if hint >= 0 && width != hint {
936-
if !(nullable && (hint == 0 || hint == -1 || width == 0)) {
937-
// Width mismatch
938-
return 0, NewSchemeError(ErrConstraintViolated, errorName, "", pos, SizeExact{hint, width})
939-
}
945+
if !nullable && hint != 0 && width != hint {
946+
return 0, NewSchemeError(ErrConstraintViolated, errorName, "", pos, SizeExact{hint, width})
940947
}
941948

942949
return width, nil
@@ -1012,6 +1019,9 @@ func (s SchemeString) CheckFunc(code ErrorCode, expected string, test func(paylo
10121019
} else {
10131020
str = string(payload)
10141021
}
1022+
if s.IsNullable() && str == "" {
1023+
return nil
1024+
}
10151025
if !test(str) {
10161026
return NewSchemeError(code, SchemeStringName, "", pos, StringErrorDetails{Actual: str, Expected: expected})
10171027
}
@@ -1989,3 +1999,201 @@ func (s SchemeMultiCheckNamesScheme) Encode(put *access.PutAccess, val any) erro
19891999
}
19902000
return nil
19912001
}
2002+
2003+
func (s SchemeString) Optional() SchemeString {
2004+
s.Width = -1
2005+
return s
2006+
}
2007+
2008+
func SEmail(optional bool) Scheme {
2009+
s := SString
2010+
if optional {
2011+
s = s.Optional()
2012+
}
2013+
return s.CheckFunc(
2014+
ErrStringEmail,
2015+
"email",
2016+
func(payloadStr string) bool {
2017+
// Use net/mail parser for RFC-compliant syntax check
2018+
_, err := mail.ParseAddress(payloadStr)
2019+
return err == nil
2020+
},
2021+
)
2022+
}
2023+
2024+
// SURI adds URI validation + normalization (prepend https:// if missing)
2025+
func SURI(optional bool) Scheme {
2026+
s := SString
2027+
if optional {
2028+
s.Optional()
2029+
}
2030+
return s.CheckFunc(
2031+
ErrStringURL,
2032+
"URI",
2033+
func(payloadStr string) bool {
2034+
// prepend https:// if missing
2035+
if !strings.HasPrefix(payloadStr, "http://") && !strings.HasPrefix(payloadStr, "https://") {
2036+
payloadStr = "https://" + payloadStr
2037+
}
2038+
parsed, err := url.ParseRequestURI(payloadStr)
2039+
return err == nil && parsed.Host != ""
2040+
},
2041+
)
2042+
}
2043+
2044+
// SDate constrains an int64 payload to a date range (Unix seconds)
2045+
// and decodes into time.Time.
2046+
func SDate(nullable bool, from, to time.Time) Scheme {
2047+
min := from.Unix()
2048+
max := to.Unix()
2049+
2050+
return SchemeGeneric{
2051+
ValidateFunc: func(seq *access.SeqGetAccess) error {
2052+
pos := seq.CurrentIndex()
2053+
payload, err := validatePrimitiveAndGetPayload(SchemeDateName, seq, types.TypeInteger, 8, nullable)
2054+
if err != nil {
2055+
return err
2056+
}
2057+
if payload == nil {
2058+
return nil // allow nullable
2059+
}
2060+
val := int64(binary.LittleEndian.Uint64(payload))
2061+
if val < min || val > max {
2062+
return NewSchemeError(ErrDateOutOfRange, SchemeDateName, "", pos,
2063+
RangeErrorDetails{Min: min, Max: max, Actual: val},
2064+
)
2065+
}
2066+
return nil
2067+
},
2068+
DecodeFunc: func(seq *access.SeqGetAccess) (any, error) {
2069+
pos := seq.CurrentIndex()
2070+
payload, err := validatePrimitiveAndGetPayload(SchemeDateName, seq, types.TypeInteger, 8, nullable)
2071+
if err != nil {
2072+
return nil, err
2073+
}
2074+
if payload == nil {
2075+
return nil, nil // allow nullable
2076+
}
2077+
val := int64(binary.LittleEndian.Uint64(payload))
2078+
if val < min || val > max {
2079+
return nil, NewSchemeError(ErrDateOutOfRange, SchemeDateName, "", pos,
2080+
RangeErrorDetails{Min: min, Max: max, Actual: val},
2081+
)
2082+
}
2083+
// decode as time.Time
2084+
return time.Unix(val, 0).UTC(), nil
2085+
},
2086+
EncodeFunc: func(put *access.PutAccess, val any) error {
2087+
if nullable && val == nil {
2088+
put.AddNullableInt64(nil)
2089+
return nil
2090+
}
2091+
switch v := val.(type) {
2092+
case int64:
2093+
if v < min || v > max {
2094+
return NewSchemeError(ErrEncode, SchemeDateName, "", -1,
2095+
RangeErrorDetails{Min: min, Max: max, Actual: v})
2096+
}
2097+
put.AddInt64(v)
2098+
case time.Time:
2099+
sec := v.Unix()
2100+
if sec < min || sec > max {
2101+
return NewSchemeError(ErrEncode, SchemeDateName, "", -1,
2102+
RangeErrorDetails{Min: min, Max: max, Actual: sec})
2103+
}
2104+
put.AddInt64(sec)
2105+
default:
2106+
return NewSchemeError(ErrEncode, SchemeDateName, "", -1, ErrTypeMisMatch)
2107+
}
2108+
return nil
2109+
},
2110+
}
2111+
}
2112+
2113+
// SchemeEnumNamedList constrains an index to a list of names, encoded in 2 bytes.
2114+
// Perfect for radio groups or select dropdowns.
2115+
type SchemeEnumNamedList struct {
2116+
FieldNames []string
2117+
Nullable bool
2118+
}
2119+
2120+
func SEnum(fieldNames []string, nullable bool) Scheme {
2121+
return SchemeEnumNamedList{FieldNames: fieldNames, Nullable: nullable}
2122+
}
2123+
2124+
func (s SchemeEnumNamedList) IsNullable() bool { return s.Nullable }
2125+
2126+
func (s SchemeEnumNamedList) Validate(seq *access.SeqGetAccess) error {
2127+
pos := seq.CurrentIndex()
2128+
payload, err := validatePrimitiveAndGetPayload(SchemeEnumNamedListName, seq, types.TypeInteger, 2, s.IsNullable())
2129+
if err != nil {
2130+
return err
2131+
}
2132+
if payload == nil {
2133+
return nil
2134+
}
2135+
idx := int(binary.LittleEndian.Uint16(payload))
2136+
if idx < 0 || idx >= len(s.FieldNames) {
2137+
return NewSchemeError(ErrConstraintViolated, SchemeEnumNamedListName, "", pos,
2138+
SizeExact{Actual: idx, Exact: len(s.FieldNames)})
2139+
}
2140+
return nil
2141+
}
2142+
2143+
func (s SchemeEnumNamedList) Decode(seq *access.SeqGetAccess) (any, error) {
2144+
payload, err := validatePrimitiveAndGetPayload(SchemeEnumNamedListName, seq, types.TypeInteger, 2, s.IsNullable())
2145+
if err != nil {
2146+
return nil, err
2147+
}
2148+
if payload == nil {
2149+
return nil, nil
2150+
}
2151+
idx := int(binary.LittleEndian.Uint16(payload))
2152+
if idx < 0 || idx >= len(s.FieldNames) {
2153+
return nil, NewSchemeError(ErrConstraintViolated, SchemeEnumNamedListName, "", -1,
2154+
SizeExact{Actual: idx, Exact: len(s.FieldNames)})
2155+
}
2156+
return s.FieldNames[idx], nil // return the name string
2157+
}
2158+
2159+
func (s SchemeEnumNamedList) Encode(put *access.PutAccess, val any) error {
2160+
if val == nil && s.Nullable {
2161+
put.AddNullableInt16(nil)
2162+
return nil
2163+
}
2164+
2165+
var idx int
2166+
switch v := val.(type) {
2167+
case int:
2168+
idx = v
2169+
case string:
2170+
idx = -1
2171+
for i, name := range s.FieldNames {
2172+
if name == v {
2173+
idx = i
2174+
break
2175+
}
2176+
}
2177+
if idx == -1 {
2178+
return NewSchemeError(ErrEncode, SchemeEnumNamedListName, "", -1, ErrTypeMisMatch)
2179+
}
2180+
default:
2181+
return NewSchemeError(ErrEncode, SchemeEnumNamedListName, "", -1, ErrTypeMisMatch)
2182+
}
2183+
2184+
if idx < 0 || idx >= len(s.FieldNames) {
2185+
return NewSchemeError(ErrEncode, SchemeEnumNamedListName, "", -1,
2186+
SizeExact{Actual: idx, Exact: len(s.FieldNames)})
2187+
}
2188+
2189+
put.AddInt16(int16(idx))
2190+
return nil
2191+
}
2192+
2193+
func SColor(nullable bool) Scheme {
2194+
s := SString
2195+
if nullable {
2196+
s = s.Optional()
2197+
}
2198+
return s.Pattern(`^#(?:[0-9a-fA-F]{3}){1,2}$`)
2199+
}

0 commit comments

Comments
 (0)