672 lines
19 KiB
Go
672 lines
19 KiB
Go
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"regexp"
|
|
"time"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
)
|
|
|
|
// ----------------------------
|
|
// Core discriminant/type
|
|
// ----------------------------
|
|
|
|
type PropertyType = string
|
|
|
|
const (
|
|
PTDateTime PropertyType = "date_time"
|
|
PTInteger PropertyType = "integer"
|
|
PTFloat PropertyType = "float"
|
|
PTMonetary PropertyType = "monetary"
|
|
PTReference PropertyType = "reference"
|
|
PTString PropertyType = "string"
|
|
PTColor PropertyType = "color"
|
|
PTObject PropertyType = "object"
|
|
)
|
|
|
|
// Value keys for SettingsT maps
|
|
const (
|
|
VKString = "string"
|
|
VKStrings = "strings"
|
|
VKColor = "color"
|
|
VKColors = "colors"
|
|
VKInteger = "integer"
|
|
VKIntegers = "integers"
|
|
VKFloat = "float"
|
|
VKFloats = "floats"
|
|
VKDateTime = "date_time"
|
|
VKDateTimes = "date_times"
|
|
VKMonetary = "monetary"
|
|
VKMonetaries = "monetaries"
|
|
VKReference = "reference"
|
|
VKReferences = "references"
|
|
VKObject = "object"
|
|
VKObjects = "objects"
|
|
)
|
|
|
|
// Money struct field keys
|
|
const (
|
|
MKAmount = "amount"
|
|
MKCurrency = "currency"
|
|
)
|
|
|
|
// ----------------------------
|
|
// Small value types (runtime values)
|
|
// ----------------------------
|
|
|
|
// ----------------------------
|
|
// Type-specific PROPS (schema/constraints)
|
|
// ----------------------------
|
|
|
|
type IntegerProps struct {
|
|
Default *int64 `bson:"default,omitempty" json:"default,omitempty"`
|
|
Min *int64 `bson:"min,omitempty" json:"min,omitempty"`
|
|
Max *int64 `bson:"max,omitempty" json:"max,omitempty"`
|
|
Allowed []int64 `bson:"allowed,omitempty" json:"allowed,omitempty"`
|
|
}
|
|
|
|
type FloatProps struct {
|
|
Default *float64 `bson:"default,omitempty" json:"default,omitempty"`
|
|
Min *float64 `bson:"min,omitempty" json:"min,omitempty"`
|
|
Max *float64 `bson:"max,omitempty" json:"max,omitempty"`
|
|
}
|
|
|
|
type StringProps struct {
|
|
Default *string `bson:"default,omitempty" json:"default,omitempty"`
|
|
Allowed []string `bson:"allowed,omitempty" json:"allowed,omitempty"`
|
|
Pattern string `bson:"pattern" json:"pattern"` // Go RE2 syntax
|
|
MinLen *int `bson:"minLen,omitempty" json:"minLen,omitempty"`
|
|
MaxLen *int `bson:"maxLen,omitempty" json:"maxLen,omitempty"`
|
|
}
|
|
|
|
type DateTimeProps struct {
|
|
Default *time.Time `bson:"default,omitempty" json:"default,omitempty"` // store UTC
|
|
Earliest *time.Time `bson:"earliest,omitempty" json:"earliest,omitempty"`
|
|
Latest *time.Time `bson:"latest,omitempty" json:"latest,omitempty"`
|
|
}
|
|
|
|
type ColorProps struct {
|
|
AllowAlpha bool `bson:"allowAlpha,omitempty" json:"allowAlpha,omitempty"`
|
|
AllowedPalette []string `bson:"allowedPalette,omitempty" json:"allowedPalette,omitempty"` // optional whitelist of hex colors
|
|
Default string `bson:"default,omitempty" json:"default,omitempty"`
|
|
}
|
|
|
|
type ObjectProps struct {
|
|
Properties []PropertySchema `bson:"properties,omitempty" json:"properties,omitempty"`
|
|
}
|
|
|
|
// Currency policy for monetary props.
|
|
type CurrencyMode string
|
|
|
|
const (
|
|
CurrencyFixed CurrencyMode = "fixed" // force one currency (FixedCurrency)
|
|
CurrencyOrg CurrencyMode = "org" // force org default currency at runtime
|
|
CurrencyFree CurrencyMode = "free" // allow any (optionally restricted by AllowedCurrencies)
|
|
)
|
|
|
|
type MonetaryProps struct {
|
|
CurrencyMode CurrencyMode `bson:"currencyMode" json:"currencyMode"`
|
|
FixedCurrency Currency `bson:"fixedCurrency" json:"fixedCurrency"` // required if fixed
|
|
AllowedCurrencies []Currency `bson:"allowedCurrencies" json:"allowedCurrencies"` // for free mode
|
|
|
|
// Optional precision/rules; if nil, infer elsewhere by ISO minor units.
|
|
Scale *int `bson:"scale,omitempty" json:"scale,omitempty"` // allowed decimal places
|
|
Rounding *int `bson:"rounding,omitempty" json:"rounding,omitempty"` // app-specific; not enforced here
|
|
|
|
Default *Money `bson:"default,omitempty" json:"default,omitempty"`
|
|
Min *Money `bson:"min,omitempty" json:"min,omitempty"`
|
|
Max *Money `bson:"max,omitempty" json:"max,omitempty"`
|
|
}
|
|
|
|
type ReferenceProps struct {
|
|
Target mservice.Type `bson:"target" json:"target"` // e.g. "accounts"
|
|
AllowedIDs []primitive.ObjectID `bson:"allowedIds,omitempty" json:"allowedIds,omitempty"` // optional whitelist
|
|
Default *primitive.ObjectID `bson:"default,omitempty" json:"default,omitempty"` // optional default VALUE
|
|
}
|
|
|
|
// ----------------------------
|
|
// UI hints (optional)
|
|
// ----------------------------
|
|
|
|
type UIHints struct {
|
|
Placeholder string `bson:"placeholder" json:"placeholder"`
|
|
Unit string `bson:"unit" json:"unit"` // "kg", "cm", "€", etc.
|
|
HiddenInList bool `bson:"hiddenInList" json:"hiddenInList"`
|
|
Filterable bool `bson:"filterable" json:"filterable"`
|
|
}
|
|
|
|
// ----------------------------
|
|
// Multiplicity (generic, applies to any type)
|
|
// ----------------------------
|
|
|
|
type Cardinality string
|
|
|
|
const (
|
|
One Cardinality = "one" // single value
|
|
Many Cardinality = "many" // array of values
|
|
)
|
|
|
|
type Multiplicity struct {
|
|
Mode Cardinality `bson:"mode" json:"mode"` // default "one"
|
|
MinItems *int `bson:"minItems,omitempty" json:"minItems,omitempty"` // only when Mode=Many
|
|
MaxItems *int `bson:"maxItems,omitempty" json:"maxItems,omitempty"` // only when Mode=Many
|
|
// Distinct within one entity's list value (meaningful for Mode=Many).
|
|
Distinct bool `bson:"distinct" json:"distinct"`
|
|
}
|
|
|
|
// ----------------------------
|
|
// Property envelope
|
|
// ----------------------------
|
|
|
|
type PropertySchema struct {
|
|
PermissionBound `bson:",inline" json:",inline"`
|
|
Describable `bson:",inline" json:",inline"`
|
|
|
|
// customer permission refernece
|
|
ValuePermissionRef *primitive.ObjectID `bson:"valuePermissionRef,omitempty" json:"valuePermissionRef,omitempty"`
|
|
|
|
// Stable machine key; unique within (organizatoinRef, type, key)
|
|
Key string `bson:"key" json:"key"`
|
|
Type PropertyType `bson:"type" json:"type"`
|
|
|
|
// Lifecycle/UX
|
|
System bool `bson:"system" json:"system"`
|
|
UI *UIHints `bson:"ui,omitempty" json:"ui,omitempty"`
|
|
|
|
// Multiplicity controls (cross-type).
|
|
Multiplicity Multiplicity `bson:"multiplicity" json:"multiplicity"`
|
|
|
|
// Discriminated payload; a BSON subdocument shaped per Type.
|
|
Props any `bson:"props" json:"props"`
|
|
}
|
|
|
|
func (*PropertySchema) Collection() string { return mservice.PropertySchemas }
|
|
|
|
// ----------------------------
|
|
// Typed accessors for Props
|
|
// ----------------------------
|
|
|
|
func invalidType(expected, actual PropertyType) error {
|
|
return merrors.InvalidDataType(fmt.Sprintf("expected type is %s while actual type is %s", expected, actual))
|
|
}
|
|
|
|
// asTypedProps is a generic function that handles type checking and casting for all property types
|
|
func asTypedProps[T any](p *PropertySchema, expectedType PropertyType) (T, error) {
|
|
var out T
|
|
if p.Type != expectedType {
|
|
return out, invalidType(expectedType, p.Type)
|
|
}
|
|
// Props is stored directly as the correct type, so we can cast it
|
|
if props, ok := p.Props.(T); ok {
|
|
return props, nil
|
|
}
|
|
return out, merrors.InvalidArgument("invalid props type")
|
|
}
|
|
|
|
// Type-specific accessor functions using the generic template
|
|
func (p *PropertySchema) AsInteger() (IntegerProps, error) {
|
|
return asTypedProps[IntegerProps](p, PTInteger)
|
|
}
|
|
|
|
func (p *PropertySchema) AsFloat() (FloatProps, error) {
|
|
return asTypedProps[FloatProps](p, PTFloat)
|
|
}
|
|
|
|
func (p *PropertySchema) AsString() (StringProps, error) {
|
|
return asTypedProps[StringProps](p, PTString)
|
|
}
|
|
|
|
func (p *PropertySchema) AsDateTime() (DateTimeProps, error) {
|
|
return asTypedProps[DateTimeProps](p, PTDateTime)
|
|
}
|
|
|
|
func (p *PropertySchema) AsMonetary() (MonetaryProps, error) {
|
|
return asTypedProps[MonetaryProps](p, PTMonetary)
|
|
}
|
|
|
|
func (p *PropertySchema) AsReference() (ReferenceProps, error) {
|
|
return asTypedProps[ReferenceProps](p, PTReference)
|
|
}
|
|
|
|
func (p *PropertySchema) AsColor() (ColorProps, error) {
|
|
return asTypedProps[ColorProps](p, PTColor)
|
|
}
|
|
|
|
func (p *PropertySchema) AsObject() (ObjectProps, error) {
|
|
return asTypedProps[ObjectProps](p, PTObject)
|
|
}
|
|
|
|
// ----------------------------
|
|
// Validation helpers (generic)
|
|
// ----------------------------
|
|
|
|
func validateMultiplicity(count int, required bool, m Multiplicity) error {
|
|
mode := m.Mode
|
|
if mode == "" {
|
|
mode = One
|
|
}
|
|
switch mode {
|
|
case One:
|
|
if count > 1 {
|
|
return merrors.DataConflict("multiple values not allowed")
|
|
}
|
|
if required && count == 0 {
|
|
return merrors.DataConflict("value required")
|
|
}
|
|
case Many:
|
|
min := 0
|
|
if m.MinItems != nil {
|
|
min = *m.MinItems
|
|
} else if required {
|
|
min = 1
|
|
}
|
|
if count < min {
|
|
return merrors.DataConflict(fmt.Sprintf("minimum %d items", min))
|
|
}
|
|
if m.MaxItems != nil && count > *m.MaxItems {
|
|
return merrors.DataConflict(fmt.Sprintf("maximum %d items", *m.MaxItems))
|
|
}
|
|
default:
|
|
return merrors.InvalidArgument(fmt.Sprintf("unknown cardinality: %q", mode))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ensureDistinct[T comparable](vals []T, distinct bool) error {
|
|
if !distinct || len(vals) < 2 {
|
|
return nil
|
|
}
|
|
seen := make(map[T]struct{}, len(vals))
|
|
for _, v := range vals {
|
|
if _, ok := seen[v]; ok {
|
|
return merrors.DataConflict("duplicate items not allowed")
|
|
}
|
|
seen[v] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ensureDistinctByKey[T any, K comparable](vals []T, key func(T) K, distinct bool) error {
|
|
if !distinct || len(vals) < 2 {
|
|
return nil
|
|
}
|
|
seen := make(map[K]struct{}, len(vals))
|
|
for _, v := range vals {
|
|
k := key(v)
|
|
if _, ok := seen[k]; ok {
|
|
return merrors.DataConflict("duplicate items not allowed")
|
|
}
|
|
seen[k] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ----------------------------
|
|
// Type validators
|
|
// ----------------------------
|
|
|
|
func (p PropertySchema) ValidateStrings(vals []string) error {
|
|
if p.Type != PTString {
|
|
return invalidType(PTString, p.Type)
|
|
}
|
|
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
|
return err
|
|
}
|
|
if err := ensureDistinct(vals, p.Multiplicity.Distinct); err != nil {
|
|
return err
|
|
}
|
|
|
|
props, err := p.AsString()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var re *regexp.Regexp
|
|
if props.Pattern != "" {
|
|
rx, rxErr := regexp.Compile(props.Pattern)
|
|
if rxErr != nil {
|
|
return merrors.InvalidArgument(fmt.Sprintf("invalid pattern: %v", rxErr))
|
|
}
|
|
re = rx
|
|
}
|
|
|
|
allow := map[string]struct{}{}
|
|
if len(props.Allowed) > 0 {
|
|
for _, a := range props.Allowed {
|
|
allow[a] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for _, v := range vals {
|
|
if len(allow) > 0 {
|
|
if _, ok := allow[v]; !ok {
|
|
return merrors.DataConflict(fmt.Sprintf("value %q not allowed", v))
|
|
}
|
|
}
|
|
if props.MinLen != nil && len(v) < *props.MinLen {
|
|
return merrors.DataConflict(fmt.Sprintf("value too short (min %d)", *props.MinLen))
|
|
}
|
|
if props.MaxLen != nil && len(v) > *props.MaxLen {
|
|
return merrors.DataConflict(fmt.Sprintf("value too long (max %d)", *props.MaxLen))
|
|
}
|
|
if re != nil && !re.MatchString(v) {
|
|
return merrors.DataConflict(fmt.Sprintf("value %q does not match pattern", v))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p PropertySchema) ValidateColors(vals []string) error {
|
|
if p.Type != PTColor {
|
|
return invalidType(PTColor, p.Type)
|
|
}
|
|
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
|
return err
|
|
}
|
|
if err := ensureDistinct(vals, p.Multiplicity.Distinct); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err := p.AsColor()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// For now, we can use the same validation as strings
|
|
// In the future, we might want to add color-specific validation
|
|
return nil
|
|
}
|
|
|
|
func (p PropertySchema) ValidateIntegers(vals []int64) error {
|
|
if p.Type != PTInteger {
|
|
return invalidType(PTInteger, p.Type)
|
|
}
|
|
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
|
return err
|
|
}
|
|
if err := ensureDistinct(vals, p.Multiplicity.Distinct); err != nil {
|
|
return err
|
|
}
|
|
|
|
props, err := p.AsInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
allow := map[int64]struct{}{}
|
|
if len(props.Allowed) > 0 {
|
|
for _, a := range props.Allowed {
|
|
allow[a] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for _, v := range vals {
|
|
if len(allow) > 0 {
|
|
if _, ok := allow[v]; !ok {
|
|
return merrors.DataConflict(fmt.Sprintf("value %d not allowed", v))
|
|
}
|
|
}
|
|
if props.Min != nil && v < *props.Min {
|
|
return merrors.DataConflict(fmt.Sprintf("value %d below min %d", v, *props.Min))
|
|
}
|
|
if props.Max != nil && v > *props.Max {
|
|
return merrors.DataConflict(fmt.Sprintf("value %d above max %d", v, *props.Max))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p PropertySchema) ValidateFloats(vals []float64) error {
|
|
if p.Type != PTFloat {
|
|
return invalidType(PTFloat, p.Type)
|
|
}
|
|
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
|
return err
|
|
}
|
|
if err := ensureDistinct(vals, p.Multiplicity.Distinct); err != nil {
|
|
return err
|
|
}
|
|
|
|
props, err := p.AsFloat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, v := range vals {
|
|
if props.Min != nil && v < *props.Min {
|
|
return merrors.DataConflict(fmt.Sprintf("value %g below min %g", v, *props.Min))
|
|
}
|
|
if props.Max != nil && v > *props.Max {
|
|
return merrors.DataConflict(fmt.Sprintf("value %g above max %g", v, *props.Max))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p PropertySchema) ValidateDateTimes(vals []time.Time) error {
|
|
if p.Type != PTDateTime {
|
|
return invalidType(PTDateTime, p.Type)
|
|
}
|
|
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
|
return err
|
|
}
|
|
// Distinct datetimes rarely matter; honor it if requested.
|
|
if err := ensureDistinctByKey(vals, func(t time.Time) int64 { return t.UTC().UnixNano() }, p.Multiplicity.Distinct); err != nil {
|
|
return err
|
|
}
|
|
|
|
props, err := p.AsDateTime()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, v := range vals {
|
|
vu := v.UTC()
|
|
if props.Earliest != nil && vu.Before(props.Earliest.UTC()) {
|
|
return merrors.DataConflict("datetime before earliest")
|
|
}
|
|
if props.Latest != nil && vu.After(props.Latest.UTC()) {
|
|
return merrors.DataConflict("datetime after latest")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Monetary validation (handles currency policy + Min/Max + optional scale)
|
|
func (p PropertySchema) ValidateMonetaries(vals []Money, orgCurrency Currency) error {
|
|
if p.Type != PTMonetary {
|
|
return invalidType(PTMonetary, p.Type)
|
|
}
|
|
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
|
return err
|
|
}
|
|
// Distinct by (currency, amount)
|
|
if err := ensureDistinctByKey(vals, func(m Money) string { return string(m.Currency) + "|" + m.Amount.String() }, p.Multiplicity.Distinct); err != nil {
|
|
return err
|
|
}
|
|
|
|
props, err := p.AsMonetary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
allowedCur := map[Currency]struct{}{}
|
|
if len(props.AllowedCurrencies) > 0 {
|
|
for _, c := range props.AllowedCurrencies {
|
|
allowedCur[c] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for _, v := range vals {
|
|
// Currency policy
|
|
switch props.CurrencyMode {
|
|
case CurrencyFixed:
|
|
if props.FixedCurrency == "" {
|
|
return merrors.InvalidArgument("fixed currency is not configured")
|
|
}
|
|
if v.Currency != props.FixedCurrency {
|
|
return merrors.DataConflict(fmt.Sprintf("currency must be %s", props.FixedCurrency))
|
|
}
|
|
case CurrencyOrg:
|
|
if orgCurrency == "" {
|
|
return merrors.InvalidArgument("org currency not provided")
|
|
}
|
|
if v.Currency != Currency(orgCurrency) {
|
|
return merrors.DataConflict(fmt.Sprintf("currency must be %s", orgCurrency))
|
|
}
|
|
case CurrencyFree, "":
|
|
if len(allowedCur) > 0 {
|
|
if _, ok := allowedCur[v.Currency]; !ok {
|
|
return merrors.DataConflict(fmt.Sprintf("currency %s not allowed", v.Currency))
|
|
}
|
|
}
|
|
default:
|
|
return merrors.InvalidArgument(fmt.Sprintf("unknown currency mode: %s", props.CurrencyMode))
|
|
}
|
|
|
|
// Scale check (if configured)
|
|
if props.Scale != nil {
|
|
ok, frac := decimal128WithinScale(v.Amount, *props.Scale)
|
|
if !ok {
|
|
return merrors.DataConflict(fmt.Sprintf("too many decimal places: got %d, max %d", frac, *props.Scale))
|
|
}
|
|
}
|
|
|
|
// Min/Max (apply only if currencies match)
|
|
if props.Min != nil && props.Min.Currency == v.Currency {
|
|
cmp, cmpErr := compareDecimal128(v.Amount, props.Min.Amount)
|
|
if cmpErr == nil && cmp < 0 {
|
|
return merrors.DataConflict("amount below min")
|
|
}
|
|
}
|
|
if props.Max != nil && props.Max.Currency == v.Currency {
|
|
cmp, cmpErr := compareDecimal128(v.Amount, props.Max.Amount)
|
|
if cmpErr == nil && cmp > 0 {
|
|
return merrors.DataConflict("amount above max")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// References: existence check is injected.
|
|
type ExistFn func(resource mservice.Type, id primitive.ObjectID, filter bson.M) (bool, error)
|
|
|
|
func (p PropertySchema) ValidateReferences(vals []primitive.ObjectID, exist ExistFn) error {
|
|
if p.Type != PTReference {
|
|
return invalidType(PTReference, p.Type)
|
|
}
|
|
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
|
return err
|
|
}
|
|
props, err := p.AsReference()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Distinct by referenced ID (and resource)
|
|
if err := ensureDistinctByKey(vals, func(r primitive.ObjectID) string { return props.Target + ":" + r.Hex() }, p.Multiplicity.Distinct); err != nil {
|
|
return err
|
|
}
|
|
|
|
allowed := map[primitive.ObjectID]struct{}{}
|
|
if len(props.AllowedIDs) > 0 {
|
|
for _, id := range props.AllowedIDs {
|
|
allowed[id] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for _, v := range vals {
|
|
if len(allowed) > 0 {
|
|
if _, ok := allowed[v]; !ok {
|
|
return merrors.DataConflict(fmt.Sprintf("id %s not allowed", v.Hex()))
|
|
}
|
|
}
|
|
if exist != nil {
|
|
ok, exErr := exist(props.Target, v, bson.M{})
|
|
if exErr != nil {
|
|
return exErr
|
|
}
|
|
if !ok {
|
|
return merrors.DataConflict("referenced document not found or disallowed")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ----------------------------
|
|
// Decimal128 utilities
|
|
// ----------------------------
|
|
|
|
// compareDecimal128 returns -1 if a < b, 0 if a == b, 1 if a > b.
|
|
func compareDecimal128(a, b primitive.Decimal128) (int, error) {
|
|
as := a.String()
|
|
bs := b.String()
|
|
|
|
af, _, err := big.ParseFloat(as, 10, 128, big.ToNearestEven)
|
|
if err != nil {
|
|
return 0, merrors.InvalidArgument(err.Error())
|
|
}
|
|
bf, _, err := big.ParseFloat(bs, 10, 128, big.ToNearestEven)
|
|
if err != nil {
|
|
return 0, merrors.InvalidArgument(err.Error())
|
|
}
|
|
return af.Cmp(bf), nil
|
|
}
|
|
|
|
// decimal128WithinScale checks if the number of fractional digits is <= scale.
|
|
func decimal128WithinScale(d primitive.Decimal128, scale int) (ok bool, fracDigits int) {
|
|
// Normalize via big.Float to handle exponents; then trim trailing zeros.
|
|
s := d.String()
|
|
f, _, err := big.ParseFloat(s, 10, 128, big.ToNearestEven)
|
|
if err != nil {
|
|
fd := countFractionDigits(s)
|
|
return fd <= scale, fd
|
|
}
|
|
fixed := f.Text('f', 40) // enough precision
|
|
fixed = trimTrailingZeros(fixed)
|
|
fd := countFractionDigits(fixed)
|
|
return fd <= scale, fd
|
|
}
|
|
|
|
func countFractionDigits(s string) int {
|
|
dot := -1
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '.' {
|
|
dot = i
|
|
break
|
|
}
|
|
}
|
|
if dot < 0 {
|
|
return 0
|
|
}
|
|
return len(s) - dot - 1
|
|
}
|
|
|
|
func trimTrailingZeros(s string) string {
|
|
dot := -1
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '.' {
|
|
dot = i
|
|
break
|
|
}
|
|
}
|
|
if dot < 0 {
|
|
return s
|
|
}
|
|
j := len(s) - 1
|
|
for j > dot && s[j] == '0' {
|
|
j--
|
|
}
|
|
if j == dot {
|
|
return s[:dot]
|
|
}
|
|
return s[:j+1]
|
|
}
|