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 }
528538func (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