niko_trust/pkg/tce/value.go
Niko Marmeladkov 3bf13fa488 Public SDK packages, proxy-aware rate limits, service login recipe
- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
  external Go projects can import the verified core; invariant tests
  updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
  sits behind a reverse proxy (off by default, header never trusted
  otherwise)
- examples/service + examples/approve: complete passwordless login round
  trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
2026-08-26 12:49:54 +03:00

154 lines
4 KiB
Go

package tce
// Value is a claim or approval payload value.
//
// The zero Value is null, which is a deliberate choice: a forgotten field
// encodes as an explicit null rather than as something that fails to encode or
// silently disappears.
//
// Value is an opaque struct rather than an interface so that the set of
// representable values is exactly the set the specification defines. There is
// no way to construct a value with a reserved tag, and no way for a caller to
// supply a type the encoder does not know how to canonicalize.
type Value struct {
tag ValueTag
str string // string contents, or the canonical number token
}
// Null returns the null value.
func Null() Value { return Value{tag: ValNull} }
// Bool returns a boolean value.
func Bool(b bool) Value {
if b {
return Value{tag: ValTrue}
}
return Value{tag: ValFalse}
}
// String returns a string value. Validation happens at encode time.
func String(s string) Value { return Value{tag: ValString, str: s} }
// Number returns a numeric value from its exact decimal source token.
//
// The token is taken as text rather than as a float64 because a JSON number
// is an arbitrary-precision decimal literal: converting through a binary float
// loses precision above 2^53 and makes the signed bytes depend on the
// implementation's rounding. Callers decoding JSON should use json.Number,
// which preserves the source token.
//
// The token is canonicalized at encode time, so Number("1.0") and Number("1")
// produce identical bytes.
func Number(token string) Value { return Value{tag: ValNumber, str: token} }
// Int returns a numeric value from an integer.
func Int(n int64) Value { return Value{tag: ValNumber, str: formatInt(n)} }
// Tag returns the value's type tag.
func (v Value) Tag() ValueTag { return v.tag }
// IsNull reports whether v is null.
func (v Value) IsNull() bool { return v.tag == ValNull }
// Bool returns the boolean contents and whether v is a boolean.
func (v Value) Bool() (bool, bool) {
switch v.tag {
case ValTrue:
return true, true
case ValFalse:
return false, true
default:
return false, false
}
}
// Str returns the string contents and whether v is a string.
func (v Value) Str() (string, bool) {
if v.tag == ValString {
return v.str, true
}
return "", false
}
// NumberToken returns the number's decimal token and whether v is a number.
//
// For a decoded value the token is always in canonical form, because the
// decoder rejects any other spelling.
func (v Value) NumberToken() (string, bool) {
if v.tag == ValNumber {
return v.str, true
}
return "", false
}
// Equal reports whether two values are identical after canonicalization.
//
// Numbers compare by canonical form, so Number("1.0") equals Number("1").
// A value that cannot be canonicalized never compares equal to anything,
// including itself, because it has no defined meaning.
func (v Value) Equal(o Value) bool {
if v.tag != o.tag {
return false
}
switch v.tag {
case ValNull, ValFalse, ValTrue:
return true
case ValString:
return v.str == o.str
case ValNumber:
a, err1 := CanonicalNumber(v.str)
b, err2 := CanonicalNumber(o.str)
if err1 != nil || err2 != nil {
return false
}
return a == b
default:
return false
}
}
// GoString renders a value for test failure messages.
func (v Value) GoString() string {
switch v.tag {
case ValNull:
return "null"
case ValTrue:
return "true"
case ValFalse:
return "false"
case ValString:
return "string(" + v.str + ")"
case ValNumber:
return "number(" + v.str + ")"
default:
return "invalid"
}
}
// formatInt renders an int64 in plain decimal without importing strconv's
// float machinery.
func formatInt(n int64) string {
if n == 0 {
return "0"
}
neg := n < 0
var buf [20]byte
i := len(buf)
// Accumulate using uint64 so that math.MinInt64 negates correctly.
var u uint64
if neg {
u = uint64(-(n + 1)) + 1
} else {
u = uint64(n)
}
for u > 0 {
i--
buf[i] = byte('0' + u%10)
u /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}