Skip to content

Commit

Permalink
Merge pull request #121 from jinaga/identity-specification
Browse files Browse the repository at this point in the history
identity specification
  • Loading branch information
michaellperry authored Jul 3, 2024
2 parents 2a2f75c + 1e8c235 commit 6f69d8d
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 15 deletions.
75 changes: 75 additions & 0 deletions Jinaga.Test/Pipelines/InverseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ namespace Jinaga.Test.Pipelines
{
public class InverseTest
{
[Fact]
public void Inverse_Identity()
{
var specification = Given<Company>.Match(company => company);

var inverses = specification.ComputeInverses();
inverses.Should().BeEmpty();
}

[Fact]
public void Inverse_SuccessorStep()
{
Expand Down Expand Up @@ -306,6 +315,72 @@ from office in facts.OfType<Office>()
});
}

[Fact]
public void Inverse_ProjectionFromIdentity()
{
var specification = Given<Office>.Select((office, facts) => new
{
Managers = office.Managers,
Headcount = office.Headcount
});

var inverses = specification.ComputeInverses();

inverses.Select(i => i.InverseSpecification.ToString().ReplaceLineEndings())
.Should().BeEquivalentTo(new[] {
"""
(headcount: Corporate.Headcount [
!E {
next: Corporate.Headcount [
next->prior: Corporate.Headcount = headcount
]
}
]) {
office: Corporate.Office [
office = headcount->office: Corporate.Office
]
} => headcount
""",
"""
(next: Corporate.Headcount) {
headcount: Corporate.Headcount [
headcount = next->prior: Corporate.Headcount
]
office: Corporate.Office [
office = headcount->office: Corporate.Office
]
} => headcount
""",
"""
(manager: Corporate.Manager [
!E {
termination: Corporate.Manager.Terminated [
termination->Manager: Corporate.Manager = manager
]
}
]) {
office: Corporate.Office [
office = manager->office: Corporate.Office
]
} => manager
""",
"""
(termination: Corporate.Manager.Terminated) {
manager: Corporate.Manager [
manager = termination->Manager: Corporate.Manager
]
office: Corporate.Office [
office = manager->office: Corporate.Office
]
} => manager
"""
});
}

[Fact]
public void Inverse_GeneratesCollectionIdentifiers()
{
Expand Down
11 changes: 3 additions & 8 deletions Jinaga.Test/Specifications/DistributionRuleTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,7 @@ with everyone
]
} => comment
with (site: Blog.Site, author: Jinaga.User) {
user: Jinaga.User [
user = author
]
} => user
} => author
}
""".Replace("\r\n", "\n"));
}
Expand Down Expand Up @@ -103,10 +100,8 @@ select comment
comment.author == author
select comment
))
.With(Given<Model.Site, User>.Match((site, author, facts) =>
from user in facts.OfType<User>()
where user == author
select user
.With(Given<Model.Site, User>.Match((site, author) =>
author
))
;
}
92 changes: 92 additions & 0 deletions Jinaga.Test/Specifications/SpecificationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,58 @@ namespace Jinaga.Test.Specifications.Specifications
{
public class SpecificationTest
{
[Fact]
public void CanSpecifyIdentity()
{
Specification<Airline, Airline> specification = Given<Airline>.Select((airline, facts) => airline);
specification.ToString().ReplaceLineEndings().Should().Be(
"""
(airline: Skylane.Airline) {
} => airline
"""
);
}

[Fact]
public void CanSpecifyIdentityWithShorthand()
{
Specification<Airline, Airline> specification = Given<Airline>.Match(airline => airline);
specification.ToString().ReplaceLineEndings().Should().Be(
"""
(airline: Skylane.Airline) {
} => airline
"""
);
}

[Fact]
public void CanSpecifyIdentityTwoParameters()
{
Specification<Airline, User, User> specification = Given<Airline, User>.Select((airline, user, facts) => user);
specification.ToString().ReplaceLineEndings().Should().Be(
"""
(airline: Skylane.Airline, user: Jinaga.User) {
} => user
"""
);
}

[Fact]
public void CanSpecifyIdentityTwoParametersWithShorthand()
{
Specification<Airline, User, User> specification = Given<Airline, User>.Match((airline, user) => user);
specification.ToString().ReplaceLineEndings().Should().Be(
"""
(airline: Skylane.Airline, user: Jinaga.User) {
} => user
"""
);
}

[Fact]
public void CanSpecifySuccessors()
{
Expand Down Expand Up @@ -806,6 +858,46 @@ public void Specification_RelationInProjection()
);
}

[Fact]
public void Specification_ProjectionsFromIdentity()
{
var specification = Given<Office>.Select((office, facts) =>
new
{
Managers = office.Managers,
Headcount = office.Headcount
});

specification.ToString().ReplaceLineEndings().Should().Be(
"""
(office: Corporate.Office) {
} => {
Headcount = {
headcount: Corporate.Headcount [
headcount->office: Corporate.Office = office
!E {
next: Corporate.Headcount [
next->prior: Corporate.Headcount = headcount
]
}
]
} => headcount
Managers = {
manager: Corporate.Manager [
manager->office: Corporate.Office = office
!E {
termination: Corporate.Manager.Terminated [
termination->Manager: Corporate.Manager = manager
]
}
]
} => manager
}
"""
);
}

[Fact]
public void Specification_DeeplyNestedProjection()
{
Expand Down
26 changes: 19 additions & 7 deletions Jinaga/Repository/SpecificationProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class SpecificationProcessor
private ImmutableList<Label> labels = ImmutableList<Label>.Empty;
private ImmutableList<Label> givenLabels = ImmutableList<Label>.Empty;

public static (ImmutableList<Label> given, ImmutableList<Match> matches, Projections.Projection projection) Queryable<TProjection>(LambdaExpression specExpression)
public static (ImmutableList<Label> given, ImmutableList<Match> matches, Projection projection) Queryable<TProjection>(LambdaExpression specExpression)
{
var processor = new SpecificationProcessor();
var symbolTable = processor.Given(specExpression.Parameters
Expand All @@ -25,7 +25,7 @@ public static (ImmutableList<Label> given, ImmutableList<Match> matches, Project
return (processor.givenLabels, result.Matches, result.Projection);
}

public static (ImmutableList<Label> given, ImmutableList<Match> matches, Projections.Projection projection) Scalar<TProjection>(LambdaExpression specExpression)
public static (ImmutableList<Label> given, ImmutableList<Match> matches, Projection projection) Scalar<TProjection>(LambdaExpression specExpression)
{
var processor = new SpecificationProcessor();
var symbolTable = processor.Given(specExpression.Parameters);
Expand All @@ -34,6 +34,15 @@ public static (ImmutableList<Label> given, ImmutableList<Match> matches, Project
return (processor.givenLabels, result.Matches, result.Projection);
}

public static (ImmutableList<Label> given, ImmutableList<Match> matches, Projection projection) Select<TProjection>(LambdaExpression specSelector)
{
var processor = new SpecificationProcessor();
var symbolTable = processor.Given(specSelector.Parameters
.Take(specSelector.Parameters.Count - 1));
var result = processor.ProcessProjection(specSelector.Body, symbolTable);
return (processor.givenLabels, ImmutableList<Match>.Empty, result);
}

private SymbolTable Given(IEnumerable<ParameterExpression> parameters)
{
givenLabels = parameters
Expand Down Expand Up @@ -68,10 +77,13 @@ private SourceContext ProcessShorthand(Expression expression, SymbolTable symbol
)
);
}
throw new SpecificationException($"A shorthand specification must select predecessors: {expression}");
else
{
return new SourceContext(ImmutableList<Match>.Empty, new SimpleProjection(reference.Label.Name, expression.Type));
}
}

private Projections.Projection ProcessProjection(Expression expression, SymbolTable symbolTable)
private Projection ProcessProjection(Expression expression, SymbolTable symbolTable)
{
if (expression is ParameterExpression parameterExpression)
{
Expand Down Expand Up @@ -424,7 +436,7 @@ private ReferenceContext ProcessReference(Expression expression, SymbolTable sym
}
}
}
throw new SpecificationException($"Unsuported reference {expression}."); ;
throw new SpecificationException($"Unsupported reference {expression}."); ;
}

private void ValidateMatches(ImmutableList<Match> matches)
Expand All @@ -445,7 +457,7 @@ private void ValidateMatches(ImmutableList<Match> matches)
}
}

private KeyValuePair<string, Projections.Projection> ProcessProjectionMember(MemberBinding binding, SymbolTable symbolTable)
private KeyValuePair<string, Projection> ProcessProjectionMember(MemberBinding binding, SymbolTable symbolTable)
{
if (binding is MemberAssignment assignment)
{
Expand All @@ -472,7 +484,7 @@ private static LambdaExpression GetLambda(Expression argument)
}
}

private static string LabelOfProjection(Projections.Projection projection)
private static string LabelOfProjection(Projection projection)
{
// Expect the projection to be a simple one.
if (projection is SimpleProjection simpleProjection)
Expand Down
18 changes: 18 additions & 0 deletions Jinaga/Specification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ public static Specification<TFact, TProjection> Match<TProjection>(Expression<Fu
.ToImmutableList();
return new Specification<TFact, TProjection>(specificationGivens, matches, projection);
}

public static Specification<TFact, TProjection> Select<TProjection>(Expression<Func<TFact, FactRepository, TProjection>> specSelector)
{
(var givens, var matches, var projection) = SpecificationProcessor.Select<TProjection>(specSelector);
var specificationGivens = givens
.Select(g => new SpecificationGiven(g, ImmutableList<ExistentialCondition>.Empty))
.ToImmutableList();
return new Specification<TFact, TProjection>(specificationGivens, matches, projection);
}
}

public static class Given<TFact1, TFact2>
Expand All @@ -54,6 +63,15 @@ public static Specification<TFact1, TFact2, TProjection> Match<TProjection>(Expr
.ToImmutableList();
return new Specification<TFact1, TFact2, TProjection>(specificationGivens, matches, projection);
}

public static Specification<TFact1, TFact2, TProjection> Select<TProjection>(Expression<Func<TFact1, TFact2, FactRepository, TProjection>> specSelector)
{
(var givens, var matches, var projection) = SpecificationProcessor.Select<TProjection>(specSelector);
var specificationGivens = givens
.Select(g => new SpecificationGiven(g, ImmutableList<ExistentialCondition>.Empty))
.ToImmutableList();
return new Specification<TFact1, TFact2, TProjection>(specificationGivens, matches, projection);
}
}

public class Specification<TFact, TProjection> : Specification
Expand Down

0 comments on commit 6f69d8d

Please sign in to comment.