🏡 index : quine-global/quintodrome.git

package criteria

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
)

type unmarshalConjunctionType []Expression

func (uc *unmarshalConjunctionType) UnmarshalJSON(data []byte) error {
	var raw []map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	var es unmarshalConjunctionType
	for _, e := range raw {
		for k, v := range e {
			k = strings.ToLower(k)
			expr := unmarshalExpression(k, v)
			if expr == nil {
				expr = unmarshalConjunction(k, v)
			}
			if expr == nil {
				return fmt.Errorf(`invalid expression key '%s'`, k)
			}
			es = append(es, expr)
		}
	}
	*uc = es
	return nil
}

// optionalConjunction is a top-level "all"/"any" value that remembers whether its
// key was present at all, so a Criteria providing both can be rejected. encoding/json
// calls UnmarshalJSON even for a JSON null, so present is set whenever the key appears
// — including as [] or null — while an absent key leaves it false.
type optionalConjunction struct {
	present bool
	rules   unmarshalConjunctionType
}

func (o *optionalConjunction) UnmarshalJSON(data []byte) error {
	o.present = true
	return json.Unmarshal(data, &o.rules)
}

func unmarshalExpression(opName string, rawValue json.RawMessage) Expression {
	m := make(map[string]any)
	err := json.Unmarshal(rawValue, &m)
	if err != nil {
		return nil
	}
	normalizeBoolFields(m)
	switch opName {
	case "is":
		return Is(m)
	case "isnot":
		return IsNot(m)
	case "gt":
		return Gt(m)
	case "lt":
		return Lt(m)
	case "contains":
		return Contains(m)
	case "notcontains":
		return NotContains(m)
	case "startswith":
		return StartsWith(m)
	case "endswith":
		return EndsWith(m)
	case "intherange":
		return InTheRange(m)
	case "before":
		return Before(m)
	case "after":
		return After(m)
	case "inthelast":
		return InTheLast(m)
	case "notinthelast":
		return NotInTheLast(m)
	case "inplaylist":
		return InPlaylist(m)
	case "notinplaylist":
		return NotInPlaylist(m)
	case "ismissing":
		normalizeAllBoolFields(m)
		return IsMissing(m)
	case "ispresent":
		normalizeAllBoolFields(m)
		return IsPresent(m)
	}
	return nil
}

func normalizeAllBoolFields(m map[string]any) {
	for k, v := range m {
		m[k] = normalizeBoolValue(v)
	}
}

func normalizeBoolFields(m map[string]any) {
	for field, value := range m {
		info, ok := LookupField(field)
		if ok && info.Boolean {
			m[field] = normalizeBoolValue(value)
		}
	}
}

// ToBool coerces a criteria value to a bool, accepting the forms criteria values take: a real bool,
// a strconv.ParseBool-parseable string, or a JSON number that is exactly 0 or 1. Any other value
// (other numbers, slices, nil, unparseable strings) returns ok=false so callers can handle it.
func ToBool(v any) (bool, bool) {
	switch val := v.(type) {
	case bool:
		return val, true
	case string:
		b, err := strconv.ParseBool(val)
		return b, err == nil
	case float64:
		switch val {
		case 1:
			return true, true
		case 0:
			return false, true
		}
	}
	return false, false
}

// normalizeBoolValue leaves non-boolean values unchanged so they flow through to their own validation.
func normalizeBoolValue(v any) any {
	if b, ok := ToBool(v); ok {
		return b
	}
	return v
}

func unmarshalConjunction(conjName string, rawValue json.RawMessage) Expression {
	var items unmarshalConjunctionType
	err := json.Unmarshal(rawValue, &items)
	if err != nil {
		return nil
	}
	switch conjName {
	case "any":
		return Any(items)
	case "all":
		return All(items)
	}
	return nil
}

func marshalExpression(name string, value map[string]any) ([]byte, error) {
	if len(value) != 1 {
		return nil, fmt.Errorf(`invalid %s expression length %d for values %v`, name, len(value), value)
	}
	b := strings.Builder{}
	b.WriteString(`{"` + name + `":{`)
	for f, v := range value {
		j, err := json.Marshal(v)
		if err != nil {
			return nil, err
		}
		b.WriteString(`"` + f + `":`)
		b.Write(j)
		break
	}
	b.WriteString("}}")
	return []byte(b.String()), nil
}

func marshalConjunction(name string, conj []Expression) ([]byte, error) {
	aux := struct {
		All []Expression `json:"all,omitempty"`
		Any []Expression `json:"any,omitempty"`
	}{}
	if name == "any" {
		aux.Any = conj
	} else {
		aux.All = conj
	}
	return json.Marshal(aux)
}