Files
act_runner/internal/eval/v2/evaluation_result.go
Christopher Homberger 933c4a5bd5 feat: allow configuring gitea schema mode (#23)
* config entries for schema change
* remove broken/unused syntetic nodejs action
* specify desired schema in model struct that is read by unmarshal
* replace params by config
* allows gitea context + env names
* act --gitea now parses a subset of gitea specific workflows

Reviewed-on: https://gitea.com/actions-oss/act-cli/pulls/23
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2026-02-14 16:23:59 +00:00

475 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package v2
import (
"fmt"
"math"
"strconv"
"strings"
)
// ValueKind represents the type of a value in the evaluation engine.
// The values mirror the C# ValueKind enum.
//
// Note: The names are kept identical to the C# implementation for easier mapping.
//
// The lexer is intentionally simple it only tokenises the subset of
// expressions that are used in GitHub Actions workflow `if:` expressions.
// It does not evaluate the expression that is left to the parser.
type ValueKind int
const (
ValueKindNull ValueKind = iota
ValueKindBoolean
ValueKindNumber
ValueKindString
ValueKindObject
ValueKindArray
)
type ReadOnlyArray[T any] interface {
GetAt(i int64) T
GetEnumerator() []T
}
type ReadOnlyObject[T any] interface {
Get(key string) T
GetKv(key string) (string, T) // Returns the actual key used (for case-insensitive objects)
GetEnumerator() map[string]T
}
type BasicArray[T any] []T
func (a BasicArray[T]) GetAt(i int64) T {
if int(i) >= len(a) {
var zero T
return zero
}
return a[i]
}
func (a BasicArray[T]) GetEnumerator() []T {
return a
}
type CaseInsensitiveObject[T any] map[string]T
func (o CaseInsensitiveObject[T]) Get(key string) T {
_, v := o.GetKv(key)
return v
}
func (o CaseInsensitiveObject[T]) GetKv(key string) (k string, v T) {
for k, v := range o {
if strings.EqualFold(k, key) {
return k, v
}
}
var zero T
return key, zero
}
func (o CaseInsensitiveObject[T]) GetEnumerator() map[string]T {
return o
}
type CaseSensitiveObject[T any] map[string]T
func (o CaseSensitiveObject[T]) Get(key string) T {
return o[key]
}
func (o CaseSensitiveObject[T]) GetKv(key string) (string, T) {
return key, o[key]
}
func (o CaseSensitiveObject[T]) GetEnumerator() map[string]T {
return o
}
// EvaluationResult holds the result of evaluating an expression node.
// It mirrors the C# EvaluationResult class.
type EvaluationResult struct {
context *EvaluationContext
level int
value interface{}
kind ValueKind
raw interface{}
omitTracing bool
}
// NewEvaluationResult creates a new EvaluationResult.
func NewEvaluationResult(context *EvaluationContext, level int, val interface{}, kind ValueKind, raw interface{}, omitTracing bool) *EvaluationResult {
er := &EvaluationResult{context: context, level: level, value: val, kind: kind, raw: raw, omitTracing: omitTracing}
if !omitTracing {
er.traceValue()
}
return er
}
// Kind returns the ValueKind of the result.
func (er *EvaluationResult) Kind() ValueKind { return er.kind }
// Raw returns the raw value that was passed to the constructor.
func (er *EvaluationResult) Raw() interface{} { return er.raw }
// Value returns the canonical value.
func (er *EvaluationResult) Value() interface{} { return er.value }
// IsFalsy implements the logic from the C# class.
func (er *EvaluationResult) IsFalsy() bool {
switch er.kind {
case ValueKindNull:
return true
case ValueKindBoolean:
return !er.value.(bool)
case ValueKindNumber:
v := er.value.(float64)
return v == 0 || isNaN(v)
case ValueKindString:
return er.value.(string) == ""
default:
return false
}
}
func isNaN(v float64) bool { return v != v }
// IsPrimitive returns true if the kind is a primitive type.
func (er *EvaluationResult) IsPrimitive() bool { return er.kind <= ValueKindString }
// IsTruthy is the negation of IsFalsy.
func (er *EvaluationResult) IsTruthy() bool { return !er.IsFalsy() }
// AbstractEqual compares two EvaluationResults using the abstract equality algorithm.
func (er *EvaluationResult) AbstractEqual(other *EvaluationResult) bool {
return abstractEqual(er.value, other.value)
}
// AbstractGreaterThan compares two EvaluationResults.
func (er *EvaluationResult) AbstractGreaterThan(other *EvaluationResult) bool {
return abstractGreaterThan(er.value, other.value)
}
// AbstractGreaterThanOrEqual
func (er *EvaluationResult) AbstractGreaterThanOrEqual(other *EvaluationResult) bool {
return er.AbstractEqual(other) || er.AbstractGreaterThan(other)
}
// AbstractLessThan
func (er *EvaluationResult) AbstractLessThan(other *EvaluationResult) bool {
return abstractLessThan(er.value, other.value)
}
// AbstractLessThanOrEqual
func (er *EvaluationResult) AbstractLessThanOrEqual(other *EvaluationResult) bool {
return er.AbstractEqual(other) || er.AbstractLessThan(other)
}
// AbstractNotEqual
func (er *EvaluationResult) AbstractNotEqual(other *EvaluationResult) bool {
return !er.AbstractEqual(other)
}
// ConvertToNumber converts the value to a float64.
func (er *EvaluationResult) ConvertToNumber() float64 { return convertToNumber(er.value) }
// ConvertToString converts the value to a string.
func (er *EvaluationResult) ConvertToString() string {
switch er.kind {
case ValueKindNull:
return ""
case ValueKindBoolean:
if er.value.(bool) {
return ExpressionConstants.True
}
return ExpressionConstants.False
case ValueKindNumber:
return fmt.Sprintf(ExpressionConstants.NumberFormat, er.value.(float64))
case ValueKindString:
return er.value.(string)
default:
return fmt.Sprintf("%v", er.value)
}
}
// TryGetCollectionInterface returns the underlying collection if the value is an array or object.
func (er *EvaluationResult) TryGetCollectionInterface() (interface{}, bool) {
switch v := er.value.(type) {
case ReadOnlyArray[any]:
return v, true
case ReadOnlyObject[any]:
return v, true
default:
return nil, false
}
}
// CreateIntermediateResult creates an EvaluationResult from an arbitrary object.
func CreateIntermediateResult(context *EvaluationContext, obj interface{}) *EvaluationResult {
val, kind, raw := convertToCanonicalValue(obj)
return NewEvaluationResult(context, 0, val, kind, raw, true)
}
// --- Helper functions and constants ---------------------------------------
// ExpressionConstants holds string constants used in conversions.
var ExpressionConstants = struct {
True string
False string
NumberFormat string
}{
True: "true",
False: "false",
NumberFormat: "%.15g",
}
// convertToCanonicalValue converts an arbitrary Go value to a canonical form.
func convertToCanonicalValue(obj interface{}) (interface{}, ValueKind, interface{}) {
switch v := obj.(type) {
case nil:
return nil, ValueKindNull, nil
case bool:
return v, ValueKindBoolean, v
case int, int8, int16, int32, int64:
f := float64(toInt64(v))
return f, ValueKindNumber, f
case uint, uint8, uint16, uint32, uint64:
f := float64(toUint64(v))
return f, ValueKindNumber, f
case float32, float64:
f := toFloat64(v)
return f, ValueKindNumber, f
case string:
return v, ValueKindString, v
case []interface{}:
return BasicArray[any](v), ValueKindArray, v
case ReadOnlyArray[any]:
return v, ValueKindArray, v
case map[string]interface{}:
return CaseInsensitiveObject[any](v), ValueKindObject, v
case ReadOnlyObject[any]:
return v, ValueKindObject, v
default:
// Fallback: treat as object
return v, ValueKindObject, v
}
}
func toInt64(v interface{}) int64 {
switch i := v.(type) {
case int:
return int64(i)
case int8:
return int64(i)
case int16:
return int64(i)
case int32:
return int64(i)
case int64:
return i
default:
return 0
}
}
func toUint64(v interface{}) uint64 {
switch i := v.(type) {
case uint:
return uint64(i)
case uint8:
return uint64(i)
case uint16:
return uint64(i)
case uint32:
return uint64(i)
case uint64:
return i
default:
return 0
}
}
func toFloat64(v interface{}) float64 {
switch f := v.(type) {
case float32:
return float64(f)
case float64:
return f
default:
return 0
}
}
// coerceTypes implements the C# CoerceTypes logic.
// It converts values to compatible types before comparison.
func coerceTypes(left, right interface{}) (interface{}, interface{}, ValueKind, ValueKind) {
leftKind := getKind(left)
rightKind := getKind(right)
// same kind nothing to do
if leftKind == rightKind {
return left, right, leftKind, rightKind
}
// Number <-> String
if leftKind == ValueKindNumber && rightKind == ValueKindString {
right = convertToNumber(right)
rightKind = ValueKindNumber
return left, right, leftKind, rightKind
}
if leftKind == ValueKindString && rightKind == ValueKindNumber {
left = convertToNumber(left)
leftKind = ValueKindNumber
return left, right, leftKind, rightKind
}
// Boolean or Null -> Number
if leftKind == ValueKindBoolean || leftKind == ValueKindNull {
left = convertToNumber(left)
return coerceTypes(left, right)
}
if rightKind == ValueKindBoolean || rightKind == ValueKindNull {
right = convertToNumber(right)
return coerceTypes(left, right)
}
// otherwise keep as is
return left, right, leftKind, rightKind
}
// abstractEqual uses coerceTypes before comparing.
func abstractEqual(left, right interface{}) bool {
left, right, leftKind, rightKind := coerceTypes(left, right)
if leftKind != rightKind {
return false
}
switch leftKind {
case ValueKindNull:
return true
case ValueKindNumber:
l := left.(float64)
r := right.(float64)
if isNaN(l) || isNaN(r) {
return false
}
return l == r
case ValueKindString:
return strings.EqualFold(left.(string), right.(string))
case ValueKindBoolean:
return left.(bool) == right.(bool)
// Compare object equality fails via panic
// case ValueKindObject, ValueKindArray:
// return left == right
}
return false
}
// abstractGreaterThan uses coerceTypes before comparing.
func abstractGreaterThan(left, right interface{}) bool {
left, right, leftKind, rightKind := coerceTypes(left, right)
if leftKind != rightKind {
return false
}
switch leftKind {
case ValueKindNumber:
l := left.(float64)
r := right.(float64)
if isNaN(l) || isNaN(r) {
return false
}
return l > r
case ValueKindString:
return strings.Compare(left.(string), right.(string)) > 0
case ValueKindBoolean:
return left.(bool) && !right.(bool)
}
return false
}
// abstractLessThan uses coerceTypes before comparing.
func abstractLessThan(left, right interface{}) bool {
left, right, leftKind, rightKind := coerceTypes(left, right)
if leftKind != rightKind {
return false
}
switch leftKind {
case ValueKindNumber:
l := left.(float64)
r := right.(float64)
if isNaN(l) || isNaN(r) {
return false
}
return l < r
case ValueKindString:
return strings.Compare(left.(string), right.(string)) < 0
case ValueKindBoolean:
return !left.(bool) && right.(bool)
}
return false
}
// convertToNumber converts a value to a float64 following JavaScript rules.
func convertToNumber(v interface{}) float64 {
switch val := v.(type) {
case nil:
return 0
case bool:
if val {
return 1
}
return 0
case float64:
return val
case float32:
return float64(val)
case string:
// parsenumber
if val == "" {
return float64(0)
}
if len(val) > 2 {
switch val[:2] {
case "0x", "0o":
if i, err := strconv.ParseInt(val, 0, 32); err == nil {
return float64(i)
}
}
}
if f, err := strconv.ParseFloat(val, 64); err == nil {
return f
}
return math.NaN()
default:
return math.NaN()
}
}
// getKind returns the ValueKind for a Go value.
func getKind(v interface{}) ValueKind {
switch v.(type) {
case nil:
return ValueKindNull
case bool:
return ValueKindBoolean
case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return ValueKindNumber
case string:
return ValueKindString
case []interface{}:
return ValueKindArray
case map[string]interface{}:
return ValueKindObject
default:
return ValueKindObject
}
}
// traceValue is a placeholder for tracing logic.
func (er *EvaluationResult) traceValue() {
// No-op in this simplified implementation.
}
// --- End of file ---------------------------------------