diffq

package module
v0.0.0-...-fb9a2ea Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 26, 2020 License: MIT Imports: 9 Imported by: 0

README

DIFFQ

Build Report Docs Version

Identify and query for changes and differences between two objects.

Diffq provides a query language to identify changes using a declarative style language. It utilizes the r3labs/diff library to identify the changes and applies a query to those changes allowing changes to be identified dynamically.

Install

go get -u github.com/cbergoon/diffq

Example

package main 

import (
	"fmt"
	"log"
	"time"

	"github.com/cbergoon/diffq"
)

type NestedType struct {
	NS  string
	NI  int
	NSS []string
}

type OuterType struct {
	S   string
	I   int
	I64 int64
	I32 int32
	B   bool
	F64 float64
	F32 float32
	SS  []string
	IS  []int

	T time.Time
	D time.Duration

	NT  NestedType
	NTP *NestedType
	NTS []*NestedType

	M map[string]int
}

func main() {
    OT1 := &OuterType{
        S:   "StringS",
        I:   1,
        I64: 64,
        I32: 32,
        B:   false,
        F64: 3.1415,
        F32: 2.718,
        T:   time.Now(),
        D:   time.Duration(time.Hour),
        SS:  []string{"SS1", "SS2", "SS3"},
        IS:  []int{1, 2, 3},
        NT: NestedType{
            NS:  "StringNS",
            NI:  123,
            NSS: []string{"NSS1", "NSS2"},
        },
        NTP: &NestedType{
            NS:  "StringNS",
            NI:  123,
            NSS: []string{"NSS1", "NSS2"},
        },
        NTS: []*NestedType{
            &NestedType{
                NS:  "AStringNS",
                NI:  123,
                NSS: []string{"ANSS1", "ANSS2"},
            },
            &NestedType{
                NS:  "BStringNS",
                NI:  123,
                NSS: []string{"BNSS1", "BNSS2"},
            },
        },
        M: make(map[string]int),
    }

    newTime, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00-04:00")

    OT2 := &OuterType{
        S:   "StringSU",
        I:   12,
        I64: 64,
        I32: 32,
        B:   true,
        F64: 100.5,
        F32: 2.718,
        T:   newTime,
        D:   time.Duration(time.Hour * 2),
        SS:  []string{"SS1U", "SS2UX", "SS3U", "SS4U"},
        IS:  []int{1, 2, 3},
        NT: NestedType{
            NS:  "StringNS",
            NI:  123,
            NSS: []string{"NSS1", "NSS2"},
        },
        NTP: nil,
        NTS: []*NestedType{
            &NestedType{
                NS:  "AStringNS",
                NI:  123,
                NSS: []string{"ANSS1u", "ans", "ANSS2"},
            },
            &NestedType{
                NS:  "BStringNS",
                NI:  123,
                NSS: []string{"BNSS1", "BNSS2"},
            },
        },
        M: make(map[string]int),
    }

    OT1.M["one"] = 1
    OT1.M["two"] = 2

    OT2.M["one"] = 2
    OT2.M["two"] = 3

    var d *diffq.Diff
    d, _ = diffq.Differential(OT1, OT2)

    examples := []string{
        `AND(
            EVAL(S ["StringS"] => "StringSU"),
            EVAL(T => t"2020-01-01T12:00:00-04:00"),
            EVAL(D => d"2h"),
            OR(
                EVAL(F64 => 100.5),
                EVAL(NTS.0.NSS.0 => "ANSS1u")
            ),
            EVAL(B => true),
            EVAL(NTP => nil),
            EVAL(I32 =!> *),
            EVAL(SS.$first => "SS1U"),
            EVAL(SS.* => *),
            EVAL(M.one => 2),
            EVAL(SS.$last => "SS4U"),
            EVAL(SS.* => $created),
            EVAL(SS.1 => "SS2UX"), 
            EVAL(F64 =LTE> 110.0)
        )`,
        `AND(
            EVAL(SS.* ["SS2"] => "SS2UX"),
        )`,
    }

    for i := 0; i < 1; i++ {
        for _, ex := range examples {

            result, err := d.EvaluateStatement(ex)
            if err != nil {
                log.Fatalf("error: failed to evaluate statement: %v", err)
            }

            fmt.Println(result)
        }
    }
}

Language Survey

The diffq language consists of two major constructs: boolean expresion statements and evaluation statements which are nested in conditional statements.

The language generally takes the form of an outer boolean operator with evaluation statements and additional boolean operators nested within.

Below is an informal definition of the language structure:

Statement   := [AND|OR]([Statement|Evaluator]+)
Evaluator   := EVAL(Identifier Previous Operator Literal)
Identifier  := [A-Z|a-z|.|-|_|*|$]+
Previous    := Literal
Operator    := [=>|=!>|=LT>|=GT>|=LTE>|=GTE>]

Example

Given a struct S and two arbitrary instances of the struct a and b:

type S struct {
    Step    string
    Status  string
    Value   int
    Dur     time.Duration
    Aliases []string
    PtrInt  int
}

Below are some valid queries demonstrating the structure of the language:

// This statement evaluates to true when the `Value` field does not change to 100.
AND(
    EVAL(Value =!> 100)
)
// This statement evaluates to true when the `Value` field goes greater than 100 and either or both of 1) the `Status` 
// field changes from "New" to "Scheduled" or 2) the `Step` field changes to "PROC-2".
AND(
    EVAL(Value =GT> 100),
    OR(
        EVAL(Status ["New"] => "Scheduled"), 
        EVAL(Step => "PROC-2")
    )
)
// This statement evaluates to true when the an element of the `Aliases` field is created. 
AND(
    EVAL(Aliases.* => $created)
)
// This statement evaluates to true when the `Step` field changes to any value.
AND(
    EVAL(Step => *)
)

Identifiers

Identifiers indicate the field by using a path-like syntax. Nested types are accessed by concatenating the field names with a '.' (period). Array and map indicies are accessed in the same manner but by using the appropriate key or modifier no square brackets or quotes required.

Special modifiers are available for arrays to access the first and last elements of an array. These are $first and $last and can be interspurced in the directly in the identifier.

AND(
    EVAL(Aliases.$first => "Test")
) 

Additionally, asterisks can be used as wildcards to match arbitrary fields or indicies in a path.

AND(
    EVAL(Aliases.* => "Test")
) 

Operators

Operators indicate how the evaluator should compare the changed values. Operators can be though of as "goes to" operators (i.e. a value "goes to" 3) with modifiers. THe full list of operators are listed below:

GOES TO:                        => 
DOES NOT GO TO:                 =!>
GOES GREATER THAN:              =GT>
GOES GREATER THAN OR EQUAL:     =GTE>
GOES LESS THAN:                 =LT>
GOES LESS THAN OR EQUAL:        =LTE>

Operators always directly follow the identifier or the previous value if present and semantically are relative to the change or new value.

Literal Values

Literal values are the represent the types that can be compared to the changed values. Literal values in the diffq language are int, float, string, boolean, time and, duration. These type are represented as shown below:

INT:        100, -123
FLOAT:      1.5, 100.5, -3.1415
STRING:     "diffq string"
BOOLEAN:    TRUE, FALSE, true, false
TIME:       t"2020-01-01T12:00:00-04:00"
DURATION:   d"24h"

There are also 4 additional types of special literal valules: asterisk, nil, $created and, $deleted. These sepcial literal values are used as show below:

ASTERISK:   EVAL(Step => *) // Step changes to any value
NIL:        EVAL(PtrInt => nil) // PtrInt goes to nil
CREATED:    EVAL(Aliases.* => $created) // An element of Aliases is created
DELETED:    EVAL(Aliases.* => $deleted) // An element of Aliases is deleted

Previous

Previous values are optional and allow the evaluator to more selectively control a match. The previous value signifies that the match must have changed from the specified value in order to be considered a match. If no previous value is provided then it is not considered when matching a rule and the previous value can be any.

EVAL(Step ["PROC-1"] => "PROC-2") // Step must change from "PROC-1" to "PROC-2"

Comments

Comments use the /* */ format and can be used within a statement.

License

MIT - See LICENSE file.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Change

type Change struct {
	// Type indicates the change type: create, delete or update.
	Type string
	// Path is an array of field names representing the path to the field in
	// question from the outer struct.
	Path []string
	// From contains the original value of the field.
	From interface{}
	// To contains the new value of the field.
	To interface{}
}

Change represents a single change identified by the differential. Change is intentionally abstracted from the r3labs/diff library to avoid tight coupling to the dependancy.

type Changes

type Changes []Change

Changes represents a list of changes identified by the differential process.

type Diff

type Diff struct {
	// Changed indicates the presence of any changes between two objects.
	Changed bool
	// Changes holds the complete list of changes identified by the diff
	// process.
	Changes Changes
	// ChangeLogMap maps the field identifiers of the changed values to the
	// change.
	ChangeLogMap map[string]Change
	// Original holds the original struct value
	Original interface{}
	// New holds the new struct value
	New interface{}
}

Diff represents the differential of two arbitrary structs and is used to evaluate statements against it.

func Differential

func Differential(a, b interface{}) (*Diff, error)

Differential calculates the differential of a and b returning an initialized Diff and an error if encountered.

Example
OT1 := &OuterType{
	S:   "StringS",
	I:   1,
	I64: 64,
	I32: 32,
	B:   false,
	F64: 3.1415,
	F32: 2.718,
	T:   time.Now(),
	D:   time.Duration(time.Hour),
	SS:  []string{"SS1", "SS2", "SS3"},
	IS:  []int{1, 2, 3},
	NT: NestedType{
		NS:  "StringNS",
		NI:  123,
		NSS: []string{"NSS1", "NSS2"},
	},
	NTP: &NestedType{
		NS:  "StringNS",
		NI:  123,
		NSS: []string{"NSS1", "NSS2"},
	},
	NTS: []*NestedType{
		{
			NS:  "AStringNS",
			NI:  123,
			NSS: []string{"ANSS1", "ANSS2"},
		},
		{
			NS:  "BStringNS",
			NI:  123,
			NSS: []string{"BNSS1", "BNSS2"},
		},
	},
	M: make(map[string]int),
}

newTime, _ := time.Parse(time.RFC3339, "2020-01-01T12:00:00-04:00")

OT2 := &OuterType{
	S:   "StringSU",
	I:   12,
	I64: 64,
	I32: 32,
	B:   true,
	F64: 100.5,
	F32: 2.718,
	T:   newTime,
	D:   time.Duration(time.Hour * 2),
	SS:  []string{"SS1U", "SS2UX", "SS3U", "SS4U"},
	IS:  []int{1, 2, 3},
	NT: NestedType{
		NS:  "StringNS",
		NI:  123,
		NSS: []string{"NSS1", "NSS2"},
	},
	NTP: nil,
	NTS: []*NestedType{
		{
			NS:  "AStringNS",
			NI:  123,
			NSS: []string{"ANSS1u", "ans", "ANSS2"},
		},
		{
			NS:  "BStringNS",
			NI:  123,
			NSS: []string{"BNSS1", "BNSS2"},
		},
	},
	M: make(map[string]int),
}

OT1.M["one"] = 1
OT1.M["two"] = 2

OT2.M["one"] = 2
OT2.M["two"] = 3

var d *Diff
d, _ = Differential(OT1, OT2)

examples := []string{
	`AND(
			EVAL(S ["StringS"] => "StringSU"),
			EVAL(T => t"2020-01-01T12:00:00-04:00"),
			EVAL(D => d"2h"),
			OR(
				EVAL(F64 => 100.5),
				EVAL(NTS.0.NSS.0 => "ANSS1u")
			),
			EVAL(B => true),
			EVAL(NTP => nil),
			EVAL(I32 =!> *),
			EVAL(SS.$first => "SS1U"),
			EVAL(SS.* => *),
			EVAL(M.one => 2),
			EVAL(SS.$last => "SS4U"),
			EVAL(SS.* => $created),
			EVAL(SS.1 => "SS2UX"), /* when the second element is SS2UX */
			EVAL(F64 =LTE> 110.0)
		)`,
	`AND(
			EVAL(SS.* ["SS2"] => "SS2UX"),
		)`,
}

for i := 0; i < 1; i++ {
	for _, ex := range examples {

		result, err := d.EvaluateStatement(ex)
		if err != nil {
			log.Fatalf("error: failed to evaluate statement: %v", err)
		}

		fmt.Println(result)
	}
}
Output:

true
true

func (*Diff) EvaluateStatement

func (d *Diff) EvaluateStatement(statement string) (bool, error)

EvaluateStatement executes statement provided against Diff, d, and returns the validity of the statement relative to the calculated diff and any errors encountered.

func (*Diff) HumanDifferential

func (d *Diff) HumanDifferential() string

HumanDifferential calculates the differential between Original and New fields on Diff, d, and returns a human readable string representing the diff.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL