Skip to content

Commit

Permalink
make coalescing context-dependent
Browse files Browse the repository at this point in the history
use coalescer instead of naked type assertions

move coalescing package

adjust tests, clarify semantics of append/prepend
  • Loading branch information
xrstf committed Nov 26, 2023
1 parent b2496b9 commit 3a61844
Show file tree
Hide file tree
Showing 29 changed files with 760 additions and 284 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ like those available in JSON (numbers, bools, objects, vectors etc.). A statemen
* **Variables** can be pre-defined or set at runtime.
* **JSONPath** expressions are first-class citizens and make referring to the current JSON document
a breeze.
* **Optional Type Safety**: Choose between pedantic, strict or humane typing for your programs.
Strict allows nearly no type conversions, humane allows for things like `1` (int) turning into
`"1"` (string) when needed.

## Installation

Expand Down Expand Up @@ -92,8 +95,9 @@ import (
const script = `(set! .foo 42) (+ $myvar 42 .foo)`

func main() {
// Rudi programs are meant to manipulate a document (path expressions like ".foo" resolve within
// that document). The document can be anything, but is most often a JSON object.
// Rudi programs are meant to manipulate a document (path expressions like
// ".foo" resolve within that document). The document can be anything,
// but is most often a JSON object.
documentData := map[string]any{"foo": 9000}

// parse the script (the name is used when generating error strings)
Expand All @@ -103,15 +107,21 @@ func main() {
}

// evaluate the program;
// this returns an evaluated value, which is the result of the last expression that was evaluated,
// plus the final document state (the updatedData) after the script has finished.
// this returns an evaluated value, which is the result of the last expression
// that was evaluated, plus the final document state (the updatedData) after
// the script has finished.
updatedData, result, err := program.Run(
documentData,
// setup the set of variables available by default in the script
rudi.NewVariables().Set("myvar", 42),
// Likewise, setup the functions available (note that this includes functions like "if" and "and",
// so running with an empty function set is generally not advisable).
// Likewise, setup the functions available (note that this includes
// functions like "if" and "and", so running with an empty function set
// is generally not advisable).
rudi.NewBuiltInFunctions(),
// decide what kind of type strictness you would like; pedantic, strict
// or humane; choose your own adventure (strict is default if you use nil
// here; humane allows conversions like 1 == "1").
coalescing.NewStrict(),
)
if err != nil {
log.Fatalf("Script failed: %v", err)
Expand Down
15 changes: 13 additions & 2 deletions aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package rudi

import (
"go.xrstf.de/rudi/pkg/coalescing"
"go.xrstf.de/rudi/pkg/eval/builtin"
"go.xrstf.de/rudi/pkg/eval/types"
)
Expand All @@ -24,9 +25,19 @@ type Function = types.Function
// Document is the global document that is being processed by a Rudi script.
type Document = types.Document

// Coalescer is responsible for type handling and equality rules. Build your own
// or use any of the predefined versions:
//
// - coalescing.NewStrict() – mostly strict, but allows nulls to be converted
// and allows ints to become floats
// - coalescing.NewPedantic() – even more strict, allows absolutely no conversions
// - coalescing.NewHumane() – gentle type handling that allows lossless
// conversions like 1 => "1" or allowing (false == nil).
type Coalescer = coalescing.Coalescer

// NewContext wraps the document, variables and functions into a Context.
func NewContext(doc Document, variables Variables, funcs Functions) Context {
return types.NewContext(doc, variables, funcs)
func NewContext(doc Document, variables Variables, funcs Functions, coalescer Coalescer) Context {
return types.NewContext(doc, variables, funcs, coalescer)
}

// NewFunctions returns an empty set of runtime functions.
Expand Down
2 changes: 1 addition & 1 deletion cmd/rudi/util/rudi.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func SetupRudiContext(files []any) (rudi.Context, error) {
vars := rudi.NewVariables().
Set("files", files)

ctx := rudi.NewContext(document, vars, rudi.NewBuiltInFunctions())
ctx := rudi.NewContext(document, vars, rudi.NewBuiltInFunctions(), nil)

return ctx, nil
}
50 changes: 25 additions & 25 deletions pkg/eval/coalescing/funcs.go → pkg/coalescing/classic.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,28 +130,28 @@ func formatFloat(f float64) string {
return strings.TrimSuffix(formatted, ".")
}

func IsEmpty(val any) (bool, error) {
switch v := val.(type) {
case bool:
return !v, nil
case int64:
return v == 0, nil
case float64:
return v == 0, nil
case nil:
return true, nil
case string:
return len(v) == 0, nil
case []any:
return len(v) == 0, nil
case map[string]any:
return len(v) == 0, nil
default:
lit, ok := val.(ast.Literal)
if !ok {
return false, fmt.Errorf("cannot determine emptiness oT %s", val)
}

return IsEmpty(lit.LiteralValue())
}
}
// func IsEmpty(val any) (bool, error) {
// switch v := val.(type) {
// case bool:
// return !v, nil
// case int64:
// return v == 0, nil
// case float64:
// return v == 0, nil
// case nil:
// return true, nil
// case string:
// return len(v) == 0, nil
// case []any:
// return len(v) == 0, nil
// case map[string]any:
// return len(v) == 0, nil
// default:
// lit, ok := val.(ast.Literal)
// if !ok {
// return false, fmt.Errorf("cannot determine emptiness oT %s", val)
// }

// return IsEmpty(lit.LiteralValue())
// }
// }
44 changes: 44 additions & 0 deletions pkg/coalescing/coalescer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2023 Christoph Mewes
// SPDX-License-Identifier: MIT

package coalescing

import (
"fmt"

"go.xrstf.de/rudi/pkg/lang/ast"
)

type Coalescer interface {
ToBool(val any) (bool, error)
ToFloat64(val any) (float64, error)
ToInt64(val any) (int64, error)
ToNumber(val any) (ast.Number, error)
ToString(val any) (string, error)
ToVector(val any) ([]any, error)
ToObject(val any) (map[string]any, error)
ToNull(val any) (bool, error)
}

func deliteral(val any) any {
lit, ok := val.(ast.Literal)
if ok {
return lit.LiteralValue()
}

return val
}

func toNumber(c Coalescer, val any) (ast.Number, error) {
i, err := c.ToInt64(val)
if err == nil {
return ast.Number{Value: i}, nil
}

f, err := c.ToFloat64(val)
if err == nil {
return ast.Number{Value: f}, nil
}

return ast.Number{}, fmt.Errorf("cannot convert %v losslessly to number", val)
}
167 changes: 167 additions & 0 deletions pkg/coalescing/humane.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: 2023 Christoph Mewes
// SPDX-License-Identifier: MIT

package coalescing

import (
"fmt"
"strconv"
"strings"

"go.xrstf.de/rudi/pkg/lang/ast"
)

type humane struct{}

func NewHumane() Coalescer {
return humane{}
}

var _ Coalescer = humane{}

func (humane) ToNull(val any) (bool, error) {
switch v := deliteral(val).(type) {
case nil:
return true, nil
case bool:
return !v, nil
default:
return false, fmt.Errorf("cannot coalesce %T into null", v)
}
}

func (humane) ToBool(val any) (bool, error) {
switch v := deliteral(val).(type) {
case bool:
return v, nil
case int:
return v != 0, nil
case int32:
return v != 0, nil
case int64:
return v != 0, nil
case float32:
return v != 0, nil
case float64:
return v != 0, nil
case string:
if v == "" || v == "0" {
return false, nil
}

return !strings.EqualFold(v, "false"), nil
case []any:
return len(v) > 0, nil
case map[string]any:
return len(v) > 0, nil
case nil:
return false, nil
default:
return false, fmt.Errorf("cannot coalesce %T into bool", v)
}
}

func (humane) ToFloat64(val any) (float64, error) {
switch v := deliteral(val).(type) {
case int:
return float64(v), nil
case int32:
return float64(v), nil
case int64:
return float64(v), nil
case float32:
return float64(v), nil
case float64:
return v, nil
case string:
parsed, err := strconv.ParseFloat(v, 64)
if err != nil {
return 0, fmt.Errorf("cannot convert %q losslessly to float64", v)
}
return parsed, nil
case bool:
if v {
return 1, nil
} else {
return 0, nil
}
case nil:
return 0, nil
default:
return 0, fmt.Errorf("cannot coalesce %T into float64", v)
}
}

func (humane) ToInt64(val any) (int64, error) {
switch v := deliteral(val).(type) {
case int:
return int64(v), nil
case int32:
return int64(v), nil
case int64:
return v, nil
case string:
parsed, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0, fmt.Errorf("cannot convert %q losslessly to int64", v)
}
return parsed, nil
case bool:
if v {
return 1, nil
} else {
return 0, nil
}
case nil:
return 0, nil
default:
return 0, fmt.Errorf("cannot coalesce %T into int64", v)
}
}

func (h humane) ToNumber(val any) (ast.Number, error) {
return toNumber(h, val)
}

func (humane) ToString(val any) (string, error) {
switch v := deliteral(val).(type) {
case string:
return v, nil
case bool:
return strconv.FormatBool(v), nil
case int:
return strconv.FormatInt(int64(v), 10), nil
case int32:
return strconv.FormatInt(int64(v), 10), nil
case int64:
return strconv.FormatInt(v, 10), nil
case float64:
return formatFloat(v), nil
case nil:
return "", nil
default:
return "", fmt.Errorf("cannot coalesce %T into string", v)
}
}

func (humane) ToVector(val any) ([]any, error) {
switch v := deliteral(val).(type) {
case nil:
return []any{}, nil
case []any:
return v, nil
default:
return nil, fmt.Errorf("cannot coalesce %T into vector", v)
}
}

func (humane) ToObject(val any) (map[string]any, error) {
switch v := deliteral(val).(type) {
case nil:
return map[string]any{}, nil
case map[string]any:
return v, nil
default:
return nil, fmt.Errorf("cannot coalesce %T into object", v)
}
}
Loading

0 comments on commit 3a61844

Please sign in to comment.