A simple, extensible system for identifying and selecting scopes within sets.
$ dotnet add package ScopeSelectionA simple, portable library to make selection of subsets easy and abstract.
When accessing a semi-hierarchical collection of items, you often want an easy way to tell if an element is in a particular subset.
Consider the following data class to support execution of Cucumber style clauses:
public class Clause
{
public string Feature { get; init; } = "";
public string Scenario { get; init; } = "";
public string Keyword { get; init; } = "";
public IReadOnlyList<string> Tags { get; init; } = [];
public string Payload { get; set; } = "";
}
Compare that with different ways it may be bound:
public class StepBinding
{
public string Feature { get; init; } = "";
public string Scenario { get; init; } = "";
public string Keyword { get; init; } = "";
public IReadOnlyList<string> Tags { get; init; } = [];
public required IBinding { get; set; }
}
public class StepArgumentTransformationBinding
{
public string Feature { get; init; } = "";
public string Scenario { get; init; } = "";
public string Keyword { get; init; } = "";
public IReadOnlyList<string> Tags { get; init; } = [];
public required ITransformation { get; set; }
}
Such structuring proliferates and promotes redundant query logic.
This can be solved by introducing an abstraction that encapsulates redundant structure.
public class Clause
{
public required Scope { get; init }
public string Payload { get; set; } = "";
}
public class StepBinding
{
public required Scope { get; init }
public required IBinding { get; set; }
}
public class StepArgumentTransformationBinding
{
public required Scope { get; init }
public required ITransformation { get; set; }
}
Encapsulating redundant structure provides an opportunity to encapsulate redundant logic, too. Instead of sophisticated logic being repeated, you can hide them behind a single method you use in a where LINQ operation.
var Bindings = AllBindings.Where(B => Operation.Scope.IsSatisfiedBy(B.Scope));
At its core, this package provides the abstraction to solve this problem, but it goes a little further.
While the fundamental purpose of the library is to provide the contract, this package also provides two built-in types of scope:
Later, an hierarchical built-in may be added.
A supply and demand scope space involves explicit declarations involving tokens. These tokens can be of any type.
Following is an example of use.
[TestMethod]
public void SelectItem()
{
var ScopeSpace = ScopeSpaces.SupplyAndDemand<ClauseType>();
var BindingScope = ScopeSpace.Supply(Given);
var Included = ScopeSpace.Demand(Given).IsSatisfiedBy(BindingScope);
Included.ShouldBeTrue();
}
[TestMethod]
public void FailToSelectItem()
{
var ScopeSpace = ScopeSpaces.SupplyAndDemand<ClauseType>();
var BindingScope = ScopeSpace.Supply([Given, Then]);
var Included = ScopeSpace.Demand(When).IsSatisfiedBy(BindingScope);
Included.ShouldBeFalse();
}
A composite scope space is multi-dimensional.
[TestMethod]
public void SelectItemInCompositeSpace()
{
var ClauseTypes = ScopeSpaces.SupplyAndDemand<ClauseType>();
var Features = ScopeSpaces.SupplyAndDemand<string>();
var CompositeSpace = ScopeSpaces.Composite(ClauseTypes, Features);
var BindingScope = CompositeSpace.Combine(ClauseTypes.Any, Features.Supply("Scope Resolution"));
var Included = CompositeSpace.Combine(ClauseTypes.Demand(Given), Features.Any).IsSatisfiedBy(BindingScope);
Included.ShouldBeTrue();
}
[TestMethod]
public void FailCompositeSelectionDueToOneDimension()
{
var ClauseTypes = ScopeSpaces.SupplyAndDemand<ClauseType>();
var Features = ScopeSpaces.SupplyAndDemand<string>();
var CompositeSpace = ScopeSpaces.Composite(ClauseTypes, Features);
var BindingScope = CompositeSpace.Combine(ClauseTypes.Unspecified, Features.Supply("Scope Resolution"));
var Included = CompositeSpace.Combine(ClauseTypes.Demand(Given), Features.Any).IsSatisfiedBy(BindingScope);
Included.ShouldBeFalse();
}
It does not matter the two underlying dimensions are. You can even have two dimensions from the exact same type of space.
Both the composite and the supply and demand scope spaces are distinct. That means that all scopes must come from the same origin space to be unioned, intersected, and compared.
Scopes from different spaces that have relational operations run on them will generate InvalidOperationExceptions. This mirrors at runtime the behavior you alreay get from the compiler when dealing with scopes of different type.
There is one more built-in type of scope: the null scope space. This models having no scope partitioning at all.
Every scope in this space is always satisfied by every other object in the same space and the spaces are not considered distinct.
Feel free to reach out to me on GitHub or via LinkedIn.