LLKennedy/imagetemplate

View on GitHub
render/conditional.go

Summary

Maintainability
A
0 mins
Test Coverage
package render

import (
    "fmt"
    "strconv"
    "strings"
)

type conditionalOperator string

const (
    equals         conditionalOperator = "equals"
    contains       conditionalOperator = "contains"
    startswith     conditionalOperator = "startswith"
    endswith       conditionalOperator = "endswith"
    ciEquals       conditionalOperator = "ci_equals"
    ciContains     conditionalOperator = "ci_contains"
    ciStartswith   conditionalOperator = "ci_startswith"
    ciEndswith     conditionalOperator = "ci_endswith"
    numequals      conditionalOperator = "=="
    lessthan       conditionalOperator = "<"
    greaterthan    conditionalOperator = ">"
    lessorequal    conditionalOperator = "<="
    greaterorequal conditionalOperator = ">="
)

type groupOperator string

const (
    or   groupOperator = "or"
    and  groupOperator = "and"
    nor  groupOperator = "nor"
    nand groupOperator = "nand"
    xor  groupOperator = "xor"
)

/*ComponentConditional enables or disables a component based on named properties.

All properties will be assumed to be either strings or floats based on the operator.

String operators: "equals", "contains", "startswith", "endswith", "ci_equals", "ci_contains", "ci_startswith", "ci_endswith". Operators including "ci_" are case-insensitive variants.

Float operators: "=", ">", "<", "<=", ">=".

Group operators can be "and", "or", "nand", "nor", "xor".*/
type ComponentConditional struct {
    // Name is the variable to check against the specified value.
    Name string `json:"name"`
    // Not determines whether to negate the final result of the boolean operation.
    Not bool `json:"boolNot"`
    // Operator specifies which comparison operation to perform.
    Operator conditionalOperator `json:"operator"`
    // Value is the condition to operate against with the variable specified by Name.
    Value string `json:"value"`
    // Group is an optional set of other conditionals to check along with this one.
    Group conditionalGroup `json:"group"`
    /*
        valueSet represents whether this individual component has had its value set
        and its condition evaluated at least once.
    */
    valueSet bool
    /*
        validated represents whether this individual component at this level is
        validated. Use ComponentConditional.Validate() to evaluate the logic of
        entire groups.
    */
    validated bool
}

type conditionalGroup struct {
    Operator     groupOperator          `json:"groupOperator"`
    Conditionals []ComponentConditional `json:"conditionals"`
}

// SetValue sets the value of a specific named property through this conditional chain, evaluating any conditions along the way.
func (c ComponentConditional) SetValue(name string, value interface{}) (conditional ComponentConditional, err error) {
    conditional = c
    for conIndex, con := range conditional.Group.Conditionals {
        conditional.Group.Conditionals[conIndex], err = con.SetValue(name, value)
        if err != nil {
            return c, err
        }
    }
    if conditional.Name == "" && !conditional.valueSet {
        conditional.validated = true
        conditional.valueSet = true
        return conditional, nil
    }
    if conditional.Name == name {
        conditional, err = conditional.setConditionalValue(value)
    }
    return conditional, err
}

func (c ComponentConditional) setConditionalValue(value interface{}) (conditional ComponentConditional, err error) {
    conditional = c
    switch conditional.Operator {
    case equals, contains, startswith, endswith, ciEquals, ciContains, ciStartswith, ciEndswith:
        conditional, err = conditional.setString(value)
    case numequals, lessthan, greaterthan, lessorequal, greaterorequal:
        conditional, err = conditional.setNum(value)
    default:
        err = fmt.Errorf("invalid conditional operator %v", conditional.Operator)
    }
    if err != nil {
        return c, err
    }
    if conditional.Not {
        conditional.validated = !conditional.validated
    }
    conditional.valueSet = true
    return conditional, nil
}

func (c ComponentConditional) setString(value interface{}) (conditional ComponentConditional, err error) {
    conditional = c
    // Handle string operators
    stringVal, ok := value.(string)
    if !ok {
        return c, fmt.Errorf("invalid value for string operator: %v", value)
    }
    conVal := conditional.Value
    switch conditional.Operator {
    case ciEquals:
        conVal = strings.ToLower(conVal)
        stringVal = strings.ToLower(stringVal)
        fallthrough
    case equals:
        conditional.validated = conVal == stringVal
    case ciContains:
        conVal = strings.ToLower(conVal)
        stringVal = strings.ToLower(stringVal)
        fallthrough
    case contains:
        conditional.validated = strings.Contains(stringVal, conVal)
    case ciStartswith:
        conVal = strings.ToLower(conVal)
        stringVal = strings.ToLower(stringVal)
        fallthrough
    case startswith:
        if len(conVal) > len(stringVal) {
            conditional.validated = false
            break
        }
        conditional.validated = stringVal[:len(conVal)] == conVal
    case ciEndswith:
        conVal = strings.ToLower(conVal)
        stringVal = strings.ToLower(stringVal)
        fallthrough
    case endswith:
        if len(conVal) > len(stringVal) {
            conditional.validated = false
            break
        }
        conditional.validated = stringVal[len(stringVal)-len(conVal):] == conVal
    }
    return conditional, nil
}

func (c ComponentConditional) setNum(value interface{}) (conditional ComponentConditional, err error) {
    conditional = c
    // Handle float operators
    floatVal, ok := value.(float64)
    if !ok {
        intVal, ok := value.(int)
        if !ok {
            return c, fmt.Errorf("invalid value for float operator: %v", value)
        }
        floatVal = float64(intVal)
    }
    conVal, err := strconv.ParseFloat(conditional.Value, 64)
    if err != nil {
        return c, fmt.Errorf("failed to convert conditional value to float: %v", conditional.Value)
    }
    switch conditional.Operator {
    case numequals:
        conditional.validated = floatVal == conVal
    case lessthan:
        conditional.validated = floatVal < conVal
    case greaterthan:
        conditional.validated = floatVal > conVal
    case lessorequal:
        conditional.validated = floatVal <= conVal
    case greaterorequal:
        conditional.validated = floatVal >= conVal
    }
    return conditional, nil
}

// Validate validates this conditional chain, erroring if a value down the line has not been set and evaluated.
func (c ComponentConditional) Validate() (result bool, err error) {
    if !c.valueSet && c.Name != "" {
        return false, fmt.Errorf("attempted to validate conditional %v %v %v without setting %v", c.Name, c.Operator, c.Value, c.Name)
    }
    var negate bool
    group := c.Group.Conditionals
    if len(group) == 0 {
        return c.validated, nil
    }
    op := c.Group.Operator
    switch op {
    case xor:
        result, err = c.validateXor()
    case nand, nor:
        negate = true
        fallthrough
    case and, or:
        result, err = c.validateAndOr(negate)
    default:
        result = false
        err = fmt.Errorf("invalid group operator %v", op)
    }
    return
}

func (c ComponentConditional) validateXor() (bool, error) {
    //Evaluate XOR on a group as meaning only one of all results in the list can be true, and one must be true.
    trueCount := 0
    if c.validated {
        trueCount++
    }
    for _, subConditional := range c.Group.Conditionals {
        result, err := subConditional.Validate()
        if err != nil {
            return false, err
        }
        if result {
            trueCount++
        }
    }
    return trueCount == 1, nil
}

func (c ComponentConditional) validateAndOr(negate bool) (bool, error) {
    result := c.validated
    for _, subConditional := range c.Group.Conditionals {
        subResult, err := subConditional.Validate()
        if err != nil {
            return false, err
        }
        if c.Group.Operator == and || c.Group.Operator == nand {
            result = result && subResult
        } else {
            result = result || subResult
        }
    }
    if negate {
        result = !result
    }
    return result, nil
}

// GetNamedPropertiesList returns a list of all named props found in the conditional.
func (c ComponentConditional) GetNamedPropertiesList() NamedProperties {
    results := NamedProperties{}
    if c.Name == "" && len(c.Group.Conditionals) == 0 {
        return results
    }
    results[c.Name] = struct {
        Message string
    }{Message: "Please replace this struct with real data"}
    for _, subConditional := range c.Group.Conditionals {
        subResults := subConditional.GetNamedPropertiesList()
        for key, value := range subResults {
            results[key] = value
        }
    }
    return results
}