Skip to content

Instantly share code, notes, and snippets.

@theandrew168
Last active May 14, 2024 22:53
Show Gist options
  • Save theandrew168/75a5d97d794e7835670eaaa6c6d53b30 to your computer and use it in GitHub Desktop.
Save theandrew168/75a5d97d794e7835670eaaa6c6d53b30 to your computer and use it in GitHub Desktop.
TurboDLP Discussion
package main
import (
"encoding/json"
"fmt"
"regexp"
)
// Algebraic data types based on:
// https://eli.thegreenplace.net/2018/go-and-algebraic-data-types/
type Rule interface {
isRule()
}
// How to decode a Rule:
// 1. Try to decude a Basic (base case)
// 2. If that succeeds, it is Basic
// 3. Otherwise, try to decode a Composite (recursive case)
// 4. If that succeeds, it is Composite
// 5. Otherwise, the rule is invalid
type Basic struct {
Pattern string `json:"pattern"`
}
func (Basic) isRule() {}
type Operation string
const (
OperationAnd Operation = "and"
OperationOr Operation = "or"
OperationNot Operation = "not"
)
type Composite struct {
Operation Operation `json:"operation"`
Rules []Rule `json:"rules"`
}
func (Composite) isRule() {}
// Recursively parse a dynamic, polymorphic rule structure.
func ParseRule(data []byte) (Rule, error) {
// base case
// try to decode a basic rule
var basic Basic
err := json.Unmarshal(data, &basic)
if err != nil {
return nil, err
}
// if the rule has a pattern, it is basic so return it
if basic.Pattern != "" {
return basic, nil
}
// recursive case
// decode a partial composite and its sub-rules
var partial struct {
Operation Operation `json:"operation"`
Rules []json.RawMessage `json:"rules"`
}
err = json.Unmarshal(data, &partial)
if err != nil {
return nil, err
}
// decode each sub-rule recursively and collect into a slice
var rules []Rule
for _, ruleData := range partial.Rules {
rule, err := ParseRule(ruleData)
if err != nil {
return nil, err
}
rules = append(rules, rule)
}
// construct and return the composite rule
composite := Composite{
Operation: partial.Operation,
Rules: rules,
}
return composite, nil
}
// Helper for checking if every item in a slice is the value we want.
func every(items []bool, want bool) bool {
for _, item := range items {
if item != want {
return false
}
}
return true
}
// Helper for checking if some items in a slice are the value we want.
func some(items []bool, want bool) bool {
for _, item := range items {
if item == want {
return true
}
}
return false
}
func evaluate(text string, rule Rule) bool {
switch r := rule.(type) {
// base case
case Basic:
match, _ := regexp.MatchString(r.Pattern, text)
return match
// recursive case
case Composite:
switch r.Operation {
case OperationAnd:
var results []bool
for _, rr := range r.Rules {
result := evaluate(text, rr)
results = append(results, result)
}
return every(results, true)
case OperationOr:
var results []bool
for _, rr := range r.Rules {
result := evaluate(text, rr)
results = append(results, result)
}
return some(results, true)
case OperationNot:
var results []bool
for _, rr := range r.Rules {
result := evaluate(text, rr)
results = append(results, result)
}
return every(results, false)
default:
panic("unexpected operation")
}
default:
panic("unexpected rule")
}
}
func main() {
var fooOrBar Rule = Composite{
Operation: OperationOr,
Rules: []Rule{
Basic{Pattern: "foo"},
Basic{Pattern: "bar"},
},
}
encoded, err := json.Marshal(fooOrBar)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(encoded))
decoded, err := ParseRule(encoded)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", decoded)
var fooAndNotBar Rule = Composite{
Operation: OperationAnd,
Rules: []Rule{
Basic{Pattern: "foo"},
Composite{
Operation: OperationNot,
Rules: []Rule{
Basic{Pattern: "bar"},
},
},
},
}
encoded, err = json.Marshal(fooAndNotBar)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(encoded))
decoded, err = ParseRule(encoded)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", decoded)
tests := []struct {
text string
rule Rule
want bool
}{
{
text: "My cat foo is not a bar.",
rule: fooOrBar,
want: true,
},
{
text: "My cat foo is not a.",
rule: fooOrBar,
want: true,
},
{
text: "My cat is not a bar.",
rule: fooOrBar,
want: true,
},
{
text: "My cat is not a.",
rule: fooOrBar,
want: false,
},
{
text: "My cat foo is not a bar.",
rule: fooAndNotBar,
want: false,
},
{
text: "My cat is not a bar.",
rule: fooAndNotBar,
want: false,
},
{
text: "My cat foo is not a.",
rule: fooAndNotBar,
want: true,
},
}
for _, test := range tests {
got := evaluate(test.text, test.rule)
if got != test.want {
fmt.Println("FAILED")
} else {
fmt.Println("SUCCESS")
}
}
}
type Basic = {
pattern: string;
}
type Operation = "and" | "or" | "not";
type Composite = {
operation: Operation;
rules: Rule[];
}
type Rule = Basic | Composite;
const fooOrBar: Rule = {
operation: "or",
rules: [{
pattern: "foo",
}, {
pattern: "bar",
}],
}
const fooAndNotBar: Rule = {
operation: "and",
rules: [{
pattern: "foo",
}, {
operation: "not",
rules: [{
pattern: "bar",
}, {
pattern: "baz",
}],
}],
}
function evaluate(text: string, rule: Rule): boolean {
// base case
if ('pattern' in rule) {
return new RegExp(rule.pattern).test(text);
}
// recursive case
switch (rule.operation) {
case "and": {
const results = rule.rules.map((rule) => evaluate(text, rule));
return results.every((res) => res);
}
case "or": {
const results = rule.rules.map((rule) => evaluate(text, rule));
return results.some((res) => res);
}
case "not": {
const results = rule.rules.map((rule) => evaluate(text, rule));
return results.every((res) => !res);
}
}
}
const tests = [{
text: "My cat foo is not a bar.",
rule: fooOrBar,
want: true,
}, {
text: "My cat foo is not a.",
rule: fooOrBar,
want: true,
}, {
text: "My cat is not a bar.",
rule: fooOrBar,
want: true,
}, {
text: "My cat is not a.",
rule: fooOrBar,
want: false,
}, {
text: "My cat foo is not a bar.",
rule: fooAndNotBar,
want: false,
}, {
text: "My cat is not a bar.",
rule: fooAndNotBar,
want: false,
}, {
text: "My cat foo is not a.",
rule: fooAndNotBar,
want: true,
}];
for (const test of tests) {
const res = evaluate(test.text, test.rule);
if (res != test.want) {
console.log('FAILED');
} else {
console.log('SUCCESS');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment