Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ because those entries describe a change made in *this* repository.
Net.Nowhereatall...` in a consumer's own code - there is no compatibility
shim; update the casing on upgrade.

### Removed

- **`SimpleRecordProvider<TRecord>` is gone.** It was an invented base class
with no analog in the Apex original - a Provider that is "nothing but a
Master Template" now implements `IRecordProvider` directly, holding its
template as a field and delegating `CreateBundle` to
`RecordFactory.CreateBundle` (the same composition idiom the bundled
`ContactDataProvider`/`AccountDataProvider` already used). Every Provider
that extended it - across the test suites and the add-on package README
examples - was converted to that form; there is no inheritance left in the
Provider layer.

### Fixed

- **`SharedAncestor` could crash under real concurrent access** —
Expand Down Expand Up @@ -334,10 +346,10 @@ workload** - hence beta.
`SharedAncestorProvider`, `FieldPredicateFactory`, and the `CopyFrom*`
value expressions - so a field is named without a bare `PropertyInfo` or
`nameof(...)` at the call site.
- **`MasterTemplate<TRecord>`** and **`SimpleRecordProvider<TRecord>`** -
ergonomic, strongly-typed wrappers (collection-initializer syntax for a
template; a Provider that is nothing but a template needs no boilerplate)
over the untyped engine underneath.
- **`MasterTemplate<TRecord>`** - an ergonomic, strongly-typed wrapper
(collection-initializer syntax for a template) over the untyped engine
underneath. (An earlier `SimpleRecordProvider<TRecord>` base class was
removed before release - see [Unreleased] - Removed.)
- `scripts/verify-doc-examples.py` / `verify-doc-links.py`, wired into CI -
every documented C# example is exercised by a real test, and every
relative doc link resolves.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ The result is test code that is:

## Provider Architecture

- Extensible Provider architecture — implement `IRecordProvider` directly, or
use `SimpleRecordProvider<T>` when a Provider is nothing but a template
- Extensible Provider architecture — a Provider implements `IRecordProvider`
directly, holding its `MasterTemplate` as a field and delegating
`CreateBundle` to `RecordFactory` (composition, no base class to inherit)
- Multi-variant Providers (`FlavouredLookupKey`, `DiscriminatorLookupKey`) —
resolve a different Provider for the same type by an arbitrary predicate or
field value
Expand Down
18 changes: 14 additions & 4 deletions Xfty.Bogus.Test/BogusReadmeExampleTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Reflection;
using Net.NowhereAtAll.Xfty.Core;
using Net.NowhereAtAll.Xfty.Core.Bundles;
using Net.NowhereAtAll.Xfty.Core.MasterTemplates;
using Net.NowhereAtAll.Xfty.Core.RecordProviders;
using Net.NowhereAtAll.Xfty.Demo;
using Net.NowhereAtAll.Xfty.Engine;
using Net.NowhereAtAll.Xfty.Lookup;

namespace Net.NowhereAtAll.Xfty.Bogus.Test;
Expand All @@ -12,13 +15,20 @@ namespace Net.NowhereAtAll.Xfty.Bogus.Test;
/// Bogus expression, not just the expression in isolation (see
/// FakeFullNameExpressionTest and its siblings for that).
/// </summary>
file sealed class ContactWithFakeDataProvider() : SimpleRecordProvider<Contact>(
new MasterTemplate<Contact>(x => x.Id)
file sealed class ContactWithFakeDataProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Contact>(x => x.Id)
{
[x => x.FirstName] = new FakeFullNameExpression(),
[x => x.Email] = new FakeEmailAddressExpression(),
})
{
};

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}

public class BogusReadmeExampleTest
Expand Down
19 changes: 16 additions & 3 deletions Xfty.EntityFrameworkCore.Test/DocumentEmbeddingProvider.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
using System.Reflection;
using Net.NowhereAtAll.Xfty.Core;
using Net.NowhereAtAll.Xfty.Core.Bundles;
using Net.NowhereAtAll.Xfty.Core.MasterTemplates;
using Net.NowhereAtAll.Xfty.Core.RecordProviders;
using Net.NowhereAtAll.Xfty.Engine;
using Net.NowhereAtAll.Xfty.Values;

namespace Net.NowhereAtAll.Xfty.EntityFrameworkCore.Test;

/// <summary>A demo Provider pairing a `Content` field with a pgvector-mapped embedding.</summary>
public sealed class DocumentEmbeddingProvider() : SimpleRecordProvider<DocumentEmbedding>(
new MasterTemplate<DocumentEmbedding>(x => x.Id)
public sealed class DocumentEmbeddingProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<DocumentEmbedding>(x => x.Id)
.Put(x => x.Content, new IncrementingStringExpression("chunk"))
.Put(x => x.Embedding, new RandomPgVectorExpression(DocumentEmbedding.EmbeddingDimensions)));
.Put(x => x.Embedding, new RandomPgVectorExpression(DocumentEmbedding.EmbeddingDimensions));

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}
15 changes: 13 additions & 2 deletions Xfty.Test/Examples/ExChildRecordsTest.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Reflection;
using Net.NowhereAtAll.Xfty.Core;
using Net.NowhereAtAll.Xfty.Core.Bundles;
using Net.NowhereAtAll.Xfty.Core.Children;
using Net.NowhereAtAll.Xfty.Core.MasterTemplates;
using Net.NowhereAtAll.Xfty.Core.RecordProviders;
using Net.NowhereAtAll.Xfty.Demo;
using Net.NowhereAtAll.Xfty.Engine;
using Net.NowhereAtAll.Xfty.Lookup;

namespace Net.NowhereAtAll.Xfty.Test.Examples;
Expand Down Expand Up @@ -86,5 +88,14 @@ public async Task Grandchildren_ChildProviderNests()
}
}

file sealed class BlankCaseProvider()
: SimpleRecordProvider<Case>(new MasterTemplate<Case>(x => x.Id));
file sealed class BlankCaseProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Case>(x => x.Id);

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}
88 changes: 65 additions & 23 deletions Xfty.Test/Examples/ExContextAwareValuesTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Reflection;
using Net.NowhereAtAll.Xfty.Core;
using Net.NowhereAtAll.Xfty.Core.Bundles;
using Net.NowhereAtAll.Xfty.Core.MasterTemplates;
Expand Down Expand Up @@ -164,24 +165,48 @@ public async Task ReadingUpFromAChild_NeedsDeferredMode()
}
}

file sealed class CaseUnderAccountProvider()
: SimpleRecordProvider<Case>(
new MasterTemplate<Case>(x => x.Id)
.PutRequired(x => x.AccountId, new DefaultRelationship(new Account())));
file sealed class CaseUnderAccountProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Case>(x => x.Id)
.PutRequired(x => x.AccountId, new DefaultRelationship(new Account()));

file sealed class AccountWithOwnerProvider()
: SimpleRecordProvider<Account>(
new MasterTemplate<Account>(x => x.Id)
{
[x => x.Name] = new IncrementingStringExpression("Acct"),
}.PutRequired(x => x.OwnerId, new DefaultRelationship(new User())));
public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

file sealed class LeafUserProvider()
: SimpleRecordProvider<User>(
new MasterTemplate<User>(x => x.Id)
{
[x => x.LastName] = new IncrementingStringExpression("User"),
});
public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}

file sealed class AccountWithOwnerProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Account>(x => x.Id)
{
[x => x.Name] = new IncrementingStringExpression("Acct"),
}.PutRequired(x => x.OwnerId, new DefaultRelationship(new User()));

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}

file sealed class LeafUserProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<User>(x => x.Id)
{
[x => x.LastName] = new IncrementingStringExpression("User"),
};

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}

/// <summary>A lookup whose Account provider carries no pre-existing field defaults, so a test controls the value-field order entirely itself.</summary>
file sealed class BlankAccountProviderLookup : IProviderLookup
Expand All @@ -193,15 +218,24 @@ file sealed class LeafUserProvider()
public ISet<ILookupKey> KeysFor(object? record) => new HashSet<ILookupKey> { LookupKey.Get<Account>() };
}

file sealed class BlankAccountProvider()
: SimpleRecordProvider<Account>(new MasterTemplate<Account>(x => x.Id));
file sealed class BlankAccountProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Account>(x => x.Id);

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}

/// <summary>An Account whose Site is copied up from the Contact that references it.</summary>
file sealed class AccountReadingChildDepartmentProvider : IRecordProvider
{
// on the Account Provider - from docs/use/context-aware-values.md "Reading up from a child"
// and docs/use/advanced/matching-values.md "Child value up onto a parent" - the doc's own
// .Put<Account>(...) chain form is kept here verbatim, so this stays off SimpleRecordProvider.
// .Put<Account>(...) chain form is kept here verbatim.
private MasterTemplate _template { get; } = new MasterTemplate(Field.Of<Account>(x => x.Id))
.Put<Account>(x => x.Name, new IncrementingStringExpression("Acct"))
.Put<Account>(x => x.Site, CopyFromDescendantExpression.From<Contact>(x => x.AccountId, x => x.Department));
Expand All @@ -214,7 +248,15 @@ public Task<Bundle> CreateBundle(GenerationContext context, List<object> templat
RecordFactory.CreateBundle(context, this._template, templateRecords);
}

file sealed class ContactUnderAccountProvider()
: SimpleRecordProvider<Contact>(
new MasterTemplate<Contact>(x => x.Id)
.PutRequired(x => x.AccountId, new DefaultRelationship(new Account())));
file sealed class ContactUnderAccountProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Contact>(x => x.Id)
.PutRequired(x => x.AccountId, new DefaultRelationship(new Account()));

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}
18 changes: 14 additions & 4 deletions Xfty.Test/Examples/ExGeneratingRecordsTest.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Net.NowhereAtAll.Xfty.Core;
using Net.NowhereAtAll.Xfty.Core.Bundles;
using Net.NowhereAtAll.Xfty.Core.MasterTemplates;
using Net.NowhereAtAll.Xfty.Core.RecordProviders;
using Net.NowhereAtAll.Xfty.Demo;
using Net.NowhereAtAll.Xfty.Engine;
using Net.NowhereAtAll.Xfty.Lookup;
using Net.NowhereAtAll.Xfty.Relationships;

Expand Down Expand Up @@ -106,7 +108,15 @@ public async Task GettingStarted_UnderstandingBundles()
}
}

file sealed class CaseWithAccountProvider()
: SimpleRecordProvider<Case>(
new MasterTemplate<Case>(x => x.Id)
.PutRequired(x => x.AccountId, new DefaultRelationship(new Account())));
file sealed class CaseWithAccountProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Case>(x => x.Id)
.PutRequired(x => x.AccountId, new DefaultRelationship(new Account()));

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}
49 changes: 38 additions & 11 deletions Xfty.Test/Examples/ExPerCallRelationshipsTest.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Reflection;
using Net.NowhereAtAll.Xfty.Core;
using Net.NowhereAtAll.Xfty.Core.Bundles;
using Net.NowhereAtAll.Xfty.Core.MasterTemplates;
using Net.NowhereAtAll.Xfty.Core.RecordProviders;
using Net.NowhereAtAll.Xfty.Demo;
using Net.NowhereAtAll.Xfty.Engine;
using Net.NowhereAtAll.Xfty.Lookup;
using Net.NowhereAtAll.Xfty.Relationships;

Expand Down Expand Up @@ -50,16 +52,41 @@ public async Task ReachingDeeper_APath()
}
}

file sealed class ContactRequiringAccountProvider()
: SimpleRecordProvider<Contact>(
new MasterTemplate<Contact>(x => x.Id)
.PutRequired(x => x.AccountId, new DefaultRelationship(new Account())));
file sealed class ContactRequiringAccountProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Contact>(x => x.Id)
.PutRequired(x => x.AccountId, new DefaultRelationship(new Account()));

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}

file sealed class AccountWithOptionalOwnerAndParentProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Account>(x => x.Id)
.PutOptional(x => x.OwnerId, new DefaultRelationship(new User()))
.PutOptional(x => x.ParentId, new DefaultRelationship(new Account()));

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

file sealed class AccountWithOptionalOwnerAndParentProvider()
: SimpleRecordProvider<Account>(
new MasterTemplate<Account>(x => x.Id)
.PutOptional(x => x.OwnerId, new DefaultRelationship(new User()))
.PutOptional(x => x.ParentId, new DefaultRelationship(new Account())));
public MasterTemplate MasterTemplate => this._template;

file sealed class LeafUserProvider()
: SimpleRecordProvider<User>(new MasterTemplate<User>(x => x.Id));
public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}

file sealed class LeafUserProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<User>(x => x.Id);

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}
23 changes: 17 additions & 6 deletions Xfty.Test/Lookup/DiscriminatorLookupKeyTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Reflection;
using Net.NowhereAtAll.Xfty.Core;
using Net.NowhereAtAll.Xfty.Core.Bundles;
using Net.NowhereAtAll.Xfty.Core.MasterTemplates;
using Net.NowhereAtAll.Xfty.Core.RecordProviders;
using Net.NowhereAtAll.Xfty.Demo;
using Net.NowhereAtAll.Xfty.Engine;
using Net.NowhereAtAll.Xfty.Lookup;

namespace Net.NowhereAtAll.Xfty.Test.Lookup;
Expand Down Expand Up @@ -66,9 +69,17 @@ public async Task Get_ResolvesTheRightProviderThroughAProviderLookup()
}
}

file sealed class PersonAccountProvider()
: SimpleRecordProvider<Account>(
new MasterTemplate<Account>(x => x.Id)
{
[x => x.Name] = "Person Default",
});
file sealed class PersonAccountProvider : IRecordProvider
{
private MasterTemplate _template { get; } = new MasterTemplate<Account>(x => x.Id)
{
[x => x.Name] = "Person Default",
};

public PropertyInfo PrimaryTargetField => this._template.PrimaryTargetField;

public MasterTemplate MasterTemplate => this._template;

public Task<Bundle> CreateBundle(GenerationContext context, List<object> templateRecords) =>
RecordFactory.CreateBundle(context, this._template, templateRecords);
}
Loading