Skip to content

Walkthrough

Gabe Stocco edited this page Mar 9, 2022 · 42 revisions

Objective

This is a quick walkthrough of using OAT to build a toll processing system and then a weigh station. The sample code is available in the tests as a runnable demo.

Toll Booth

We start by creating a tool booth that calculates the toll dynamically based on the rules which apply to a vehicle based on its properties.

VehicleRule

Each rule for a toll type has a cost associated with it. This isn’t a field in the Rule, but we can extend the class.

    public class VehicleRule : Rule
    {
        public int Cost;

        public VehicleRule(string name) : base(name)
        {
        }
    }

Our Target Object

In this case we are going to be operating on Vehicles. So we create a simple vehicle class with a mix of fields and properties.

class Vehicle
{
    public int Weight;
    public int Axles { get; set; }
    public int Occupants { get; set; }
    public int Capacity { get; set; }
    public Driver? Driver { get; set; }
    public VehicleType VehicleType { get; internal set; }
}

enum VehicleType
{
    Motorcycle,
    Car,
    Truck,
    Bus
}

Our Rules

Lets start with a basic rule. Cars need to pay a toll.

new VehicleRule("Normal Car"){
    Cost = 3,
    Severity = 1,
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause(Operation.Equals, "VehicleType")
        {
            Data = new List<string>()
            {
                "Car"
            }
        }
    }
}

Now this leaves out motorcycles, so lets add a smaller toll for them

new VehicleRule("Motorcycle"){
    Cost = 1,
    Severity = 0,
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause(Operation.Equals, "VehicleType")
        {
            Data = new List<string>()
            {
                "Motorcycle"
            }
        }
    }
}

Trucks cause a lot of wear on roads, so we could add a higher toll

new VehicleRule("Regular Truck")
{
    Cost = 10,
    Severity = 5,
    Expression = "IsTruck",
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause(Operation.Equals, "VehicleType")
        {
            Label = "IsTruck",
            Data = new List<string>()
            {
                "Truck"
            }
        }
    }
}

Cars towing things also generally pay a higher toll

new VehicleRule("Car with Trailer")
{
    Cost = 10,
    Severity = 3,
    Expression = "IsCar AND Axles",
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause(Operation.Equals, "VehicleType")
        {
            Label = "IsCar",
            Data = new List<string>()
            {
                "Car"
            }
        },
        new Clause(Operation.GreaterThan, "Axles")
        {
            Label = "Axles",
            Data = new List<string>()
            {
                "2"
            }
        }
    }
},

And finally we want to encourage carpooling so we add a lower toll for carpools.

new VehicleRule("Carpool Car"){
    Cost = 2,
    Severity = 2,
    Target = "Vehicle",
    Expression = "IsCar AND OccupantsGT2",
    Clauses = new List<Clause>()
    {
        new Clause(Operation.Equals, "VehicleType")
        {
            Label = "IsCar",
            Data = new List<string>()
            {
                "Car"
            }
        },
        new Clause(Operation.GreaterThan, "Occupants")
        {
            Label = "OccupantsGT2",
            Data = new List<string>()
            {
                "2"
            }
        },
    }
}

Putting It Together

Now we can take our list of rules and calculate the cost for each vehicle.

MaxBy is from MoreLinq

// This gets the maximum severity rule that is applied
//  and gets the cost of that rule, if no rules 0 cost
int GetCost(Vehicle vehicle, Analyzer analyzer, IEnumerable<Rule> rules)
{
    return ((VehicleRule)analyzer.Analyze(rules, vehicle)
        .MaxBy(x => x.Severity).FirstOrDefault())?.Cost ?? 0;
}

Let's create a couple vehicles and see how much toll they would pay

var truck = new Vehicle()
{
    VehicleType = VehicleType.Truck,
    Axles = 5,
    Occupants = 1
};

var car = new Vehicle()
{
    VehicleType = VehicleType.Car,
    Axles = 2,
    Occupants = 1
};

var carpool = new Vehicle()
{
    VehicleType = VehicleType.Car,
    Axles = 2,
    Occupants = 3
};

var motorcycle = new Vehicle()
{
    VehicleType = VehicleType.Motorcycle,
    Axles = 2,
    Occupants = 1
};

var analyzer = new Analyzer();

Assert.AreEqual(10, GetCost(truck, analyzer, rules));
Assert.AreEqual(3, GetCost(car, analyzer, rules));
Assert.AreEqual(2, GetCost(carpool, analyzer, rules));
Assert.AreEqual(1, GetCost(motorcycle, analyzer, rules));

Weigh Station

Let's say we want to leverage our toll processor and create some logic for a weigh station. We have more data available and need some more structures to hold it.

class Vehicle
{
    public int Weight;
    public int Axles { get; set; }
    public int Occupants { get; set; }
    public int Capacity { get; set; }
    public Driver? Driver { get; set; }
    public VehicleType VehicleType { get; internal set; }
}

enum VehicleType
{
    Motorcycle,
    Car,
    Truck
}

class Driver
{
    public DriverLicense? License { get; set; }
}

class DriverLicense
{
    public Endorsements Endorsements { get; set; }
    public DateTime Expiration { get; set; }
}

[Flags]
enum Endorsements
{
    Motorcycle = 1,
    Auto = 2,
    CDL = 4
}

Extending Logical Analyzer with an Operation

The next step might be to create a rule that checks if a truck is over its capacity.

We can accomplish this by extending Logical Analyzer with custom Delegates.

Delegate Declaration

Our delegate for the overweight operation for example looks like this:

public OperationResult OverweightOperationDelegate(Clause clause, 
    object? state1, object? state2, IEnumerable<ClauseCapture>? captures)
{
    if (state1 is Vehicle vehicle)
    {
        var res = vehicle.Weight > vehicle.Capacity;
        if ((res && !clause.Invert) || (clause.Invert && !res))
        {
            // The rule applies and is true and the capture is available 
            //  if capture is enabled
            return new OperationResult(true, clause.Capture ? 
               new TypedClauseCapture<int>(clause, vehicle.Weight, state1, state2) :
               null);
        }
    }
    return new OperationResult(false, null);
}

Validation Delegate Definition

Next we define a validation delegate to ensure our rules are valid

public IEnumerable<Violation> OverweightOperationValidationDelegate(Rule r, Clause c)
{
    var violations = new List<Violation>();
    if (r.Target != "Vehicle")
    {
        violations.Add(new Violation("Overweight operation requires a Vehicle object", r, c));
    }

    if (c.Data != null || c.DictData != null)
    {
        violations.Add(new Violation("Overweight operation takes no data.", r, c));
    }
    return violations;
}

Now we can instantiate our operation

var analyzer = new Analyzer();
OatOperation OverweightOperation = new OatOperation(Operation.Custom, analyzer)
{
    CustomOperation = "OVERWEIGHT",
    OperationDelegate = OverweightOperationDelegate,
    ValidationDelegate = OverweightOperationValidationDelegate
};
analyzer.SetOperation(OverWeightOperation);

Rule

And the rule for overweight vehicles looks like this:

new VehicleRule("Overweight Truck")
{
    Cost = 50,
    Severity = 9,
    Expression = "Overweight AND IsTruck",
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause(Operation.Custom)
        {
            Label = "Overweight",
            CustomOperation = "OVERWEIGHT"
        },
        new Clause(Operation.Equals, "VehicleType")
        {
            Label = "IsTruck",
            Data = new List<string>()
            {
                "Truck"
            }
        }
    }
}

Now we test that we can validate our rules

var issues = analyzer.EnumerateRuleIssues(rules);
Assert.IsFalse(issues.Any());

Processing

Now that we're ready to process let's make an overweight truck and check it with the analyzer.

var overweightTruck = new Vehicle()
{
    Weight = 30000,
    Capacity = 20000,
    Axles = 5,
    Occupants = 1
};

var analyzer = new Analyzer();
analyzer.SetOperation(OverWeightOperation);
Assert. AreEqual(50, GetCost(overweightTruck, analyzer, rules));

Accessing an Enum Flags subproperty

For our first rule, we want want to check to make sure the driver has a CDL.

We can directly access the Endorsements field using dot notation and use the Contains operation on the Enum Flags.

Note that the Invert property here inverts the result of the clause.

new VehicleRule("No CDL")
{
    Cost = 100,
    Severity = 3,
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause("Driver.License.Endorsements", Operation.Contains)
        {
            Invert = true,
            Data = new List<string>()
            {
                "CDL"
            }
        }
    }
}

Accessing a DateTime subproperty

Next we might also want to check that the license isn't expired.

new VehicleRule("Expired License"){
    Cost = 75,
    Severity = 1,
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause("Driver.License.Expiration", Operation.IsExpired);
    }
}

Processing

Lets Create some trucks and run them through.

var truck = new Vehicle()
{
    Weight = 20000,
    Capacity = 20000,
    VehicleType = VehicleType.Truck,
    Driver = new Driver()
    {
        License = new DriverLicense()
        {
            Endorsements = Endorsements.CDL | Endorsements.Auto,
            Expiration = DateTime.Now.AddYears(1)
        }
    }
};

Assert.IsTrue(!analyzer.Analyze(rules, truck).Any()); // Compliant - no rules matched

var overweightTruck = new Vehicle()
{
    Weight = 30000,
    Capacity = 20000,
    VehicleType = VehicleType.Truck,
    Driver = new Driver()
    {
        License = new DriverLicense()
        {
            Endorsements = Endorsements.CDL | Endorsements.Auto,
            Expiration = DateTime.Now.AddYears(1)
        }
    }
};

Assert.IsTrue(analyzer.Analyze(rules, overweightTruck).Any(x => x.Name == "Overweight Truck"));

var expiredLicense = new Vehicle()
{
    Weight = 20000,
    Capacity = 20000,
    VehicleType = VehicleType.Truck,
    Driver = new Driver()
    {
        License = new DriverLicense()
        {
            Endorsements = Endorsements.CDL | Endorsements.Auto,
            Expiration = DateTime.Now.AddYears(-1)
        }
    }
};

Assert.IsTrue(analyzer.Analyze(rules, expiredLicense).Any(x => x.Name == "Expired License"));

var noCdl = new Vehicle()
{
    Weight = 20000,
    Capacity = 20000,
    VehicleType = VehicleType.Truck,
    Driver = new Driver()
    {
        License = new DriverLicense()
        {
            Endorsements = Endorsements.Auto,
            Expiration = DateTime.Now.AddYears(1)
        }
    }
};

Assert.IsTrue(analyzer.Analyze(rules, noCdl).Any(x => x.Name == "No CDL"));

var analyzer = new Analyzer();
analyzer.SetOperation(OverWeightOperation);

Assert.IsFalse(analyzer.EnumerateRuleIssues(rules).Any()); // No issues with our rules

Capturing

Suppose we are interested in what the weight is when the vehicle is overweight.

We can enable capturing on our overweight rule

new VehicleRule("Overweight")
{
    Cost = 50,
    Severity = 9,
    Expression = "Overweight",
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause(Operation.Custom)
        {
            Label = "Overweight",
            CustomOperation = "OVERWEIGHT",
            Capture = true
        }
    }
}

Then let's capture the weight.

var res = analyzer.GetCaptures(rules, overweightTruck);
// We get the First Rule capture and the First Clause Capture
// Since this is an operation on an int we know we should get back a TypedClauseCapture<int>
var weight = ((TypedClauseCapture<int>)res.First().Captures[0]).Result;

Assert.AreEqual(30000, weight);

Using a Script

Instead of instantiating an OatOperation you can also provide a C# Script to evaluate.

new VehicleRule("Overweight")
{
    Cost = 50,
    Severity = 9,
    Expression = "Overweight",
    Target = "Vehicle",
    Clauses = new List<Clause>()
    {
        new Clause(Operation.Custom)
        {
            Label = "Overweight",
            Script = new ScriptData(code: @"          
if (state1 is Vehicle vehicle)
{
    var res = vehicle.Weight > vehicle.Capacity;
    if ((res && !clause.Invert) || (clause.Invert && !res))
    {
        // The rule applies and is true and the capture is available if capture is enabled
        return new OperationResult(true, clause.Capture ? 
          new TypedClauseCapture<int>(clause, vehicle.Weight, state1, state2) : null);
    }
}
return new OperationResult(false, null);",
                imports: new List<string>() {"System", "Microsoft.CST.OAT.Tests"},
                references: new List<string>(){ "OAT.Tests" }),
            Capture = true
        }
    }
}