diff --git a/docs/api/index.md b/docs/api/index.md index 7e7602fc..47fb694a 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -5,17 +5,29 @@ Main entry point API is `Stunt.Of`, which creates an object that implements ` ```csharp ICalculator calculator = Stunt.Of(); -// Extension methods to easily add behaviors without having -// to cast to IStunt -calculator.AddBehavior((invocation, next) => ...); +Console.WriteLine(calculator.Add(2, 5)); +``` + +Behaviors are configured through `IStunt`, which every stunt implements. `Stunt.For` +creates the stunt and returns a `StuntReference` that exposes both the behaviors and +the stunt itself (via `ToObject()`, or an implicit conversion for non-interface types): + +```csharp +var stunt = Stunt.For(); +ICalculator calculator = stunt.ToObject(); + +stunt.AddBehavior((invocation, next) => ...); Console.WriteLine(calculator.Add(2, 5)); ``` +`Stunt.Get(calculator)` returns the same `StuntReference` for an already created stunt, +so behaviors can be added after the fact. + There are overloads for implementing additional types, as well as passing constructor arguments if the base type `T` (which must be the first in the list, like in regular C# type declarations) is a class that provides a constructor with matching parameters: -`Stunt.Of(arg1, ... argn)` +`Stunt.Of(arg1, ... argn)` and `Stunt.For(arg1, ... argn)` For anonymous behaviors, the delegate/lambda based overloads are typically sufficient. For more advanced or reusable behaviors, you can implement `IStuntBehavior` instead. diff --git a/src/Acceptance/Dynamic.Basic/Test.vb b/src/Acceptance/Dynamic.Basic/Test.vb index fd8ff673..9aff6d49 100644 --- a/src/Acceptance/Dynamic.Basic/Test.vb +++ b/src/Acceptance/Dynamic.Basic/Test.vb @@ -8,13 +8,14 @@ Namespace Sample Public Sub CanConfigureDefaultValues() - Dim calculator = Stunt.[Of](Of ICalculator, IDisposable)() + Dim reference = Stunt.[For](Of ICalculator, IDisposable)() + Dim calculator As ICalculator = reference.ToObject() Assert.IsNotType(Of CompiledStuntFactory)(StuntFactory.[Default]) Dim recorder = New RecordingBehavior() - calculator.AddBehavior(recorder) - calculator.AddBehavior(New DefaultValueBehavior()) + reference.AddBehavior(recorder) + reference.AddBehavior(New DefaultValueBehavior()) Assert.IsAssignableFrom(Of IDisposable)(calculator) diff --git a/src/Acceptance/Dynamic/Tests.cs b/src/Acceptance/Dynamic/Tests.cs index c7ff7076..aa263a96 100644 --- a/src/Acceptance/Dynamic/Tests.cs +++ b/src/Acceptance/Dynamic/Tests.cs @@ -9,13 +9,14 @@ public class Tests [Fact] public void CanConfigureDefaultValues() { - var calculator = Stunt.Of(); + var stunt = Stunt.For(); + ICalculator calculator = stunt.ToObject(); Assert.IsNotType(StuntFactory.Default); var recorder = new RecordingBehavior(); - calculator.AddBehavior(recorder); - calculator.AddBehavior(new DefaultValueBehavior()); + stunt.AddBehavior(recorder); + stunt.AddBehavior(new DefaultValueBehavior()); Assert.IsAssignableFrom(calculator); diff --git a/src/Acceptance/Static/Tests.cs b/src/Acceptance/Static/Tests.cs index 59e670d7..6d8e84e6 100644 --- a/src/Acceptance/Static/Tests.cs +++ b/src/Acceptance/Static/Tests.cs @@ -9,12 +9,13 @@ public class Tests [Fact] public void CanConfigureDefaultValues() { - var calculator = Stunt.Of(); + var stunt = Stunt.For(); + ICalculator calculator = stunt.ToObject(); var recorder = new RecordingBehavior(); - calculator.AddBehavior(recorder); - calculator.AddBehavior(new DefaultValueBehavior()); + stunt.AddBehavior(recorder); + stunt.AddBehavior(new DefaultValueBehavior()); Assert.IsAssignableFrom(calculator); diff --git a/src/Stunts.CompiledProxy/Stunts.CompiledProxy.csproj b/src/Stunts.CompiledProxy/Stunts.CompiledProxy.csproj index cca67762..acc48983 100644 --- a/src/Stunts.CompiledProxy/Stunts.CompiledProxy.csproj +++ b/src/Stunts.CompiledProxy/Stunts.CompiledProxy.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/src/Stunts.CompiledProxy/Stunts.CompiledProxy.targets b/src/Stunts.CompiledProxy/Stunts.CompiledProxy.targets index bbb1b4b0..c58327d4 100644 --- a/src/Stunts.CompiledProxy/Stunts.CompiledProxy.targets +++ b/src/Stunts.CompiledProxy/Stunts.CompiledProxy.targets @@ -18,7 +18,6 @@ - diff --git a/src/Stunts.Package/Stunts.Package.msbuildproj b/src/Stunts.Package/Stunts.Package.msbuildproj index 15685cc0..5473e2fd 100644 --- a/src/Stunts.Package/Stunts.Package.msbuildproj +++ b/src/Stunts.Package/Stunts.Package.msbuildproj @@ -10,8 +10,10 @@ and runs everywhere, even where run-time code generation (Reflection.Emit) is fo limitted, like physical iOS devices and game consoles, through compile-time code generation. The proxy behavior is configured in code using what we call a behavior pipeline. - IMyService service = Stunt.Of<IMyService>(); -service.AddBehavior(...); + ICalculator calculator = Stunt + .For<ICalculator>() + .AddBehavior((invocation, next) => ..., appliesTo: invocation => ...) + .ToObject(); dotnet proxy runtime dynamic diff --git a/src/Stunts.UnitTests/Castle/DelegateTests.cs b/src/Stunts.UnitTests/Castle/DelegateTests.cs index df266b24..b8ad2af4 100644 --- a/src/Stunts.UnitTests/Castle/DelegateTests.cs +++ b/src/Stunts.UnitTests/Castle/DelegateTests.cs @@ -27,11 +27,11 @@ public void Run() public void BehaviorHandlesACallWithNoTarget() { var seen = false; - var action = Stunt.Of().AddBehavior((invocation, next) => + Action action = Stunt.For().AddBehavior((invocation, next) => { seen = invocation.MethodBase.Name == "Invoke"; return invocation.CreateReturn(); - }); + }).ToObject(); action(); @@ -42,11 +42,11 @@ public void BehaviorHandlesACallWithNoTarget() public void NoTargetLeavesTheTailEmpty() { bool? hasImplementation = null; - var action = Stunt.Of().AddBehavior((invocation, next) => + Action action = Stunt.For().AddBehavior((invocation, next) => { hasImplementation = invocation.HasImplementation; return next(invocation, next); - }); + }).ToObject(); Assert.Throws(() => action()); Assert.False(hasImplementation); @@ -56,7 +56,7 @@ public void ImplementationIsTheTail() { var called = false; Action implementation = () => called = true; - var action = Stunt.Of(implementation); + Action action = Stunt.Of(implementation); action(); @@ -68,13 +68,13 @@ public void BehaviorWrapsTheImplementation() { var order = new List(); Action implementation = () => order.Add("impl"); - var action = Stunt.Of(implementation).AddBehavior((invocation, next) => + Action action = Stunt.For(implementation).AddBehavior((invocation, next) => { order.Add("before"); var result = next(invocation, next); order.Add("after"); return result; - }); + }).ToObject(); action(); @@ -85,11 +85,11 @@ public void BehaviorCanReplaceAnArgument() { var seen = 0; Action implementation = value => seen = value; - var action = Stunt.Of>(implementation).AddBehavior((invocation, next) => + Action action = Stunt.For>(implementation).AddBehavior((invocation, next) => { invocation.Arguments.SetValue("obj", 41); return next(invocation, next); - }); + }).ToObject(); action(1); @@ -104,11 +104,11 @@ public void RefAndReturnAreWrittenBack() value = result; return result; }; - var action = Stunt.Of(implementation).AddBehavior((invocation, next) => + RefImpl action = Stunt.For(implementation).AddBehavior((invocation, next) => { invocation.Arguments.SetValue("value", 4); return next(invocation, next); - }); + }).ToObject(); var value = 1; var returned = action(ref value); @@ -130,7 +130,7 @@ public void ParameterNamedMethodForwards() { var seen = 0; Named implementation = method => seen = method; - var action = Stunt.Of(implementation); + Named action = Stunt.Of(implementation); action(7); @@ -141,7 +141,7 @@ public void ParametersThatHideFieldsForward() { var seen = ""; HidesFields target = (left, right) => seen = left + ":" + right; - var action = Stunt.Of(target); + HidesFields action = Stunt.Of(target); action(2, 9); @@ -160,7 +160,7 @@ public void Run() { var called = false; Hidden implementation = () => called = true; - var action = Stunt.Of(implementation); + Hidden action = Stunt.Of(implementation); action(); diff --git a/src/Stunts.UnitTests/Castle/LanguageFeaturesTests.cs b/src/Stunts.UnitTests/Castle/LanguageFeaturesTests.cs index 1724b323..9676049e 100644 --- a/src/Stunts.UnitTests/Castle/LanguageFeaturesTests.cs +++ b/src/Stunts.UnitTests/Castle/LanguageFeaturesTests.cs @@ -18,41 +18,43 @@ public void Run() public void CovariantReturnUsesTheDerivedSignature() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); + StringReturn value = stunt.ToObject(); - Assert.Equal("derived", stunt.Value()); + Assert.Equal("derived", value.Value()); stunt.AddBehavior((invocation, next) => invocation.CreateValueReturn("proxy")); - Assert.Equal("proxy", stunt.Value()); + Assert.Equal("proxy", value.Value()); } public void PositionalRecordConstructorAndVirtualMethod() { - var stunt = Stunt.Of("Ada"); + var stunt = Stunt.For("Ada"); + Person person = stunt.ToObject(); - Assert.Equal("Ada", stunt.Name); - Assert.Equal("Hello Ada", stunt.Greet()); + Assert.Equal("Ada", person.Name); + Assert.Equal("Hello Ada", person.Greet()); stunt.AddBehavior((invocation, next) => invocation.MethodBase.Name == nameof(Person.Greet) ? invocation.CreateValueReturn("hi") : next(invocation, next)); - Assert.Equal("hi", stunt.Greet()); - Assert.Equal("Ada", stunt.Name); + Assert.Equal("hi", person.Greet()); + Assert.Equal("Ada", person.Name); - var copy = stunt with { Name = "Bea" }; + var copy = person with { Name = "Bea" }; Assert.Equal("Bea", copy.Name); - Assert.IsType(stunt.GetType(), copy); + Assert.IsType(person.GetType(), copy); // The synthesized record copy copies the pipeline field. Assert.Equal("hi", copy.Greet()); } public void EmptyRecord() { - var stunt = Stunt.Of(); + Empty stunt = Stunt.Of(); Assert.IsAssignableFrom(stunt); Assert.IsAssignableFrom(stunt); @@ -60,7 +62,7 @@ public void EmptyRecord() public void RecordCanTakeAnAdditionalInterface() { - var stunt = Stunt.Of("Ada"); + Person stunt = Stunt.Of("Ada"); Assert.IsAssignableFrom(stunt); Assert.IsAssignableFrom(stunt); diff --git a/src/Stunts.UnitTests/Castle/Unsupported.md b/src/Stunts.UnitTests/Castle/Unsupported.md index 7f48c7da..10c0f419 100644 --- a/src/Stunts.UnitTests/Castle/Unsupported.md +++ b/src/Stunts.UnitTests/Castle/Unsupported.md @@ -31,6 +31,7 @@ The tests next to this file are the Castle scenarios the generator can carry. Th | Covariant returns | `LanguageFeaturesTests` | | Records (positional, empty, record plus an extra interface; `with` keeps the stunt type) | `LanguageFeaturesTests` | | `ref readonly`, `Span` and other `ref struct` arguments and returns, pointer parameters | `Scenarios/SignatureRefs` | +| Custom attributes on generated types, members, parameters, and returns; optional parameter defaults (including reflection invocation) | `AttributeTests` | Behaviors added with `AddBehavior` after `Stunt.Of` do not see virtual calls made from a base constructor. Castle passes interceptors into proxy creation, so they do. `BehaviorPipelineFactory` is the matching extension point, and `ClassProxyTests` uses it. @@ -46,24 +47,28 @@ Castle's `IProxyGenerationHook` omits members from the proxy type. Stunts overri ### Metadata Castle copies onto the proxy -The scaffold copies signatures, names, and generic constraints. It does not copy: +The generator copies C#-expressible metadata, including custom attributes and optional +parameter default values (covered by `AttributeTests`). Remaining metadata gaps are: -- custom attributes on methods, properties, parameters, or the type (`[Serializable]`, `[Guid]`, parameter attributes) -- metadata-only custom modifiers (`modopt` / `modreq`) that have no C# spelling. `in`, `ref readonly`, and function-pointer calling conventions are emitted as those keywords, and the compiler writes the matching modifier -- optional-parameter default values (callers compiled against the original type still get the compiler default; reflection on the proxy method does not) +- metadata-only custom modifiers (`modopt` / `modreq`) with no C# spelling - extra attributes supplied at generation time (`ProxyGenerationOptions.AdditionalAttributes`) ### Serialization -Castle's proxy serialization (BinaryFormatter, `ISerializable`, restoring interceptors, mixins, selector, and generation options, including graphs with delegates back to the proxy) is a generator feature. Compile-time stunts are ordinary classes with none of that infrastructure. XML serialization of a proxy is the same kind of gap: the generated type does not replicate the attributes and constructor shape `XmlSerializer` expects. +Castle-specific proxy serialization (`ISerializable`, restoring interceptors, mixins, +selector, and generation options, including graphs with delegates back to the proxy) is +not implemented by compile-time stunts. Copying `[Serializable]` and other attributes +does not provide that proxy-serialization infrastructure. ### Delegates as additional type arguments `Stunt.Of()` returns a delegate bound to a generated `Invoke`, and `Stunt.Of(implementation)` forwards `next` to that delegate. A delegate combined with a class or another interface is `ST010`. Castle can also mix a delegate `Invoke` onto some other proxy (`AsDelegate` is the missing cast). -### Same signature on two interfaces +### Cannot distinguish identical interface slots -One public method implements both. Both calls are intercepted, and a behavior cannot tell which interface was used. Castle emits explicit implementations so `IInvocation.Method` differs. +Identical signatures from two interfaces share one generated implementation. Both calls +are intercepted, but a behavior cannot tell which interface slot was called. Castle can +emit explicit implementations so `IInvocation.Method` differs. ### Fixed additional-interface arity diff --git a/src/Stunts.UnitTests/RefReturns.cs b/src/Stunts.UnitTests/RefReturns.cs index f6a53f4a..63bab668 100644 --- a/src/Stunts.UnitTests/RefReturns.cs +++ b/src/Stunts.UnitTests/RefReturns.cs @@ -9,15 +9,16 @@ public class RefReturns [Fact] public void CanReturnRef() { - INumbers stunt = new NumbersStunt(); - + var stunt = new NumbersStunt(); Ref value = 25; stunt.AddBehavior((invocation, next) => invocation.CreateValueReturn(value, invocation.Arguments)); + INumbers numbers = stunt; + var index = 0; - ref int v = ref stunt.NumberAt(ref index, out var count); + ref int v = ref numbers.NumberAt(ref index, out var count); v = 42; diff --git a/src/Stunts.UnitTests/Sample.cs b/src/Stunts.UnitTests/Sample.cs index 8a5812ca..08fda671 100644 --- a/src/Stunts.UnitTests/Sample.cs +++ b/src/Stunts.UnitTests/Sample.cs @@ -1,4 +1,5 @@ using System; +using Sample; using Stunts.Sample; using Xunit; diff --git a/src/Stunts.UnitTests/Scenarios/ClassBaseAbstractType.cs b/src/Stunts.UnitTests/Scenarios/ClassBaseAbstractType.cs index 13e7e0f0..52710a0f 100644 --- a/src/Stunts.UnitTests/Scenarios/ClassBaseAbstractType.cs +++ b/src/Stunts.UnitTests/Scenarios/ClassBaseAbstractType.cs @@ -10,10 +10,11 @@ public class Test : IRunnable public void Run() { var stunt = Stunt.Of(); + CalculatorBase calculator = stunt; - Assert.Throws(() => stunt.Mode = CalculatorMode.Scientific); - Assert.Throws(() => stunt.Mode); - Assert.Throws(() => stunt.TurnOn()); + Assert.Throws(() => calculator.Mode = CalculatorMode.Scientific); + Assert.Throws(() => calculator.Mode); + Assert.Throws(() => calculator.TurnOn()); } } } diff --git a/src/Stunts.UnitTests/Scenarios/ClassBaseType.cs b/src/Stunts.UnitTests/Scenarios/ClassBaseType.cs index e0dc18f6..40effd43 100644 --- a/src/Stunts.UnitTests/Scenarios/ClassBaseType.cs +++ b/src/Stunts.UnitTests/Scenarios/ClassBaseType.cs @@ -40,28 +40,29 @@ public class Test : IRunnable public void Run() { var stunt = Stunt.Of(); + BaseType instance = stunt; - stunt.Platform = PlatformID.MacOSX; + instance.Platform = PlatformID.MacOSX; - Assert.Equal(PlatformID.MacOSX, stunt.Platform); + Assert.Equal(PlatformID.MacOSX, instance.Platform); - Assert.False(stunt.IsOn); + Assert.False(instance.IsOn); var on = false; - stunt.TurnedOn += (_, _) => on = true; - stunt.TurnOn(); - Assert.True(stunt.IsOn); + instance.TurnedOn += (_, _) => on = true; + instance.TurnOn(); + Assert.True(instance.IsOn); Assert.True(on); var x = 5; var y = 10; var mem = 42; - Assert.True(stunt.TryAdd(x, y, ref mem, out var z)); + Assert.True(instance.TryAdd(x, y, ref mem, out var z)); Assert.Equal(15, z); - stunt[0, "foo"] = "bar"; + instance[0, "foo"] = "bar"; - Assert.Equal("bar", stunt[0, "foo"]); + Assert.Equal("bar", instance[0, "foo"]); } } } diff --git a/src/Stunts.UnitTests/Scenarios/ClassBaseTypeCtors.cs b/src/Stunts.UnitTests/Scenarios/ClassBaseTypeCtors.cs index b298c4df..2c36904b 100644 --- a/src/Stunts.UnitTests/Scenarios/ClassBaseTypeCtors.cs +++ b/src/Stunts.UnitTests/Scenarios/ClassBaseTypeCtors.cs @@ -20,15 +20,15 @@ public class Test : IRunnable { public void Run() { - var stunt = Stunt.Of("Foo"); + BaseTypeCtor instance = Stunt.Of("Foo"); - Assert.Equal("Foo", stunt.Name); - Assert.True(stunt.Enabled); + Assert.Equal("Foo", instance.Name); + Assert.True(instance.Enabled); - stunt = Stunt.Of("Foo", false); + instance = Stunt.Of("Foo", false); - Assert.Equal("Foo", stunt.Name); - Assert.False(stunt.Enabled); + Assert.Equal("Foo", instance.Name); + Assert.False(instance.Enabled); } } } \ No newline at end of file diff --git a/src/Stunts.UnitTests/Scenarios/DefaultInterfaceImplementation.cs b/src/Stunts.UnitTests/Scenarios/DefaultInterfaceImplementation.cs index 10da2eec..3b9ebe59 100644 --- a/src/Stunts.UnitTests/Scenarios/DefaultInterfaceImplementation.cs +++ b/src/Stunts.UnitTests/Scenarios/DefaultInterfaceImplementation.cs @@ -46,7 +46,7 @@ public void Run() public void DefaultsRunWhenNotIntercepted() { - var stunt = Stunt.Of(); + IDefault stunt = Stunt.Of(); Assert.Equal(5, stunt.Value); Assert.Equal("Hello Ada", stunt.Greet("Ada")); @@ -57,10 +57,10 @@ public void DefaultsRunWhenNotIntercepted() public void DefaultsCanBeReplaced() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => + IDefault stunt = Stunt.For().AddBehavior((invocation, next) => invocation.MethodBase.Name == "get_Value" ? invocation.CreateValueReturn(42) - : next(invocation, next)); + : next(invocation, next)).ToObject(); Assert.Equal(42, stunt.Value); Assert.Equal("Hello Ada", stunt.Greet("Ada")); @@ -68,13 +68,13 @@ public void DefaultsCanBeReplaced() public void DefaultsCanBeProceededTo() { - var stunt = Stunt.Of().AddBehavior((invocation, next) => + IDefault stunt = Stunt.For().AddBehavior((invocation, next) => { var result = next(invocation, next); return invocation.MethodBase.Name == nameof(IDefault.Greet) ? invocation.CreateValueReturn(result.ReturnValue + "!") : result; - }); + }).ToObject(); Assert.Equal("Hello Ada!", stunt.Greet("Ada")); Assert.Equal(5, stunt.Value); @@ -83,12 +83,12 @@ public void DefaultsCanBeProceededTo() public void DefaultEventIsIntercepted() { var added = 0; - var stunt = Stunt.Of().AddBehavior((invocation, next) => + IDefault stunt = Stunt.For().AddBehavior((invocation, next) => { if (invocation.MethodBase.Name == "add_Changed") added++; return next(invocation, next); - }); + }).ToObject(); stunt.Changed += (sender, args) => { }; @@ -97,7 +97,7 @@ public void DefaultEventIsIntercepted() public void DerivedInterfaceDefaultIsMostSpecific() { - var stunt = Stunt.Of(); + IDerived stunt = Stunt.Of(); Assert.Equal(6, stunt.Value); Assert.Equal("Hello Ada", stunt.Greet("Ada")); @@ -105,27 +105,29 @@ public void DerivedInterfaceDefaultIsMostSpecific() public void GenericInterfaceDefault() { - var stunt = Stunt.Of>(); + var stunt = Stunt.For>(); + IEcho echo = stunt.ToObject(); - Assert.Equal("Ada", stunt.Echo("Ada")); + Assert.Equal("Ada", echo.Echo("Ada")); stunt.AddBehavior((invocation, next) => invocation.CreateValueReturn("Bea")); - Assert.Equal("Bea", stunt.Echo("Ada")); + Assert.Equal("Bea", echo.Echo("Ada")); } public void ClassWithAdditionalInterfaceDefault() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); + Impl impl = stunt.ToObject(); - Assert.Equal(5, ((IDefault)stunt).Value); + Assert.Equal(5, ((IDefault)impl).Value); stunt.AddBehavior((invocation, next) => invocation.MethodBase.Name == "get_Value" ? invocation.CreateValueReturn(42) : next(invocation, next)); - Assert.Equal(42, ((IDefault)stunt).Value); + Assert.Equal(42, ((IDefault)impl).Value); } public void DefaultImplementationClass() diff --git a/src/Stunts.UnitTests/Scenarios/InterfaceBase.cs b/src/Stunts.UnitTests/Scenarios/InterfaceBase.cs index 194ea881..6395fe19 100644 --- a/src/Stunts.UnitTests/Scenarios/InterfaceBase.cs +++ b/src/Stunts.UnitTests/Scenarios/InterfaceBase.cs @@ -18,21 +18,22 @@ public class Test : IRunnable public void Run() { using var ambient = BehaviorPipelineFactory.UseAmbient(new RecordingBehaviorPipelineFactory()); - var stunt = Stunt.Of(); + var stunt = Stunt.For(); + IBasicInterface basic = stunt.ToObject(); - Assert.NotNull(stunt); - Assert.IsAssignableFrom(stunt); + Assert.NotNull(basic); + Assert.IsAssignableFrom(basic); // Recorder tracks call to constructor. - Assert.Single(((IStunt)stunt).Behaviors); - Assert.Single(((RecordingBehavior)((IStunt)stunt).Behaviors[0]).Invocations); + Assert.Single(stunt.Behaviors); + Assert.Single(((RecordingBehavior)stunt.Behaviors[0]).Invocations); // If no returning behavior is configured, invoking it throws. - Assert.Throws(() => stunt.Run()); + Assert.Throws(() => basic.Run()); // When we add at least one matching behavior, invocations succeed. stunt.AddBehavior(new DefaultValueBehavior()); - stunt.Run(); + basic.Run(); } class RecordingBehaviorPipelineFactory : IBehaviorPipelineFactory diff --git a/src/Stunts.UnitTests/Scenarios/NestedType.cs b/src/Stunts.UnitTests/Scenarios/NestedType.cs index ff77bfac..944b301d 100644 --- a/src/Stunts.UnitTests/Scenarios/NestedType.cs +++ b/src/Stunts.UnitTests/Scenarios/NestedType.cs @@ -10,10 +10,12 @@ public class Test : IRunnable { public void Run() { - var stunt = Stunt.Of() - .AddBehavior(new DefaultValueBehavior()); + var stunt = Stunt.For() + .AddBehavior(new DefaultValueBehavior()).ToObject(); - stunt.Do(); + IFoo foo = stunt; + + foo.Do(); } public interface IFoo diff --git a/src/Stunts.UnitTests/Scenarios/RefReturns.cs b/src/Stunts.UnitTests/Scenarios/RefReturns.cs index 17a68587..4b1a6ad5 100644 --- a/src/Stunts.UnitTests/Scenarios/RefReturns.cs +++ b/src/Stunts.UnitTests/Scenarios/RefReturns.cs @@ -16,10 +16,12 @@ public class Test : IRunnable { public void Run() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); stunt.AddBehavior(new DefaultValueBehavior()); - ref int value = ref stunt.Get(); + IMemory memory = stunt.ToObject(); + + ref int value = ref memory.Get(); Assert.Equal(0, value); } } diff --git a/src/Stunts.UnitTests/Scenarios/RefReturnsOut.cs b/src/Stunts.UnitTests/Scenarios/RefReturnsOut.cs index 7398c360..0c000ee6 100644 --- a/src/Stunts.UnitTests/Scenarios/RefReturnsOut.cs +++ b/src/Stunts.UnitTests/Scenarios/RefReturnsOut.cs @@ -16,11 +16,13 @@ public class Test : IRunnable { public void Run() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); stunt.AddBehavior(new DefaultValueBehavior()); + IMemory memory = stunt.ToObject(); + var name = "foo"; - ref int value = ref stunt.Get(ref name, out var _); + ref int value = ref memory.Get(ref name, out var _); Assert.Equal(0, value); } diff --git a/src/Stunts.UnitTests/Scenarios/RefReturnsValue.cs b/src/Stunts.UnitTests/Scenarios/RefReturnsValue.cs index b59d4bd7..6ce34868 100644 --- a/src/Stunts.UnitTests/Scenarios/RefReturnsValue.cs +++ b/src/Stunts.UnitTests/Scenarios/RefReturnsValue.cs @@ -16,12 +16,14 @@ public class Test : IRunnable { public void Run() { - var stunt = Stunt.Of(); + var stunt = Stunt.For(); Ref original = 12; stunt.AddBehavior((invocation, next) => invocation.CreateValueReturn(original)); - ref int value = ref stunt.Get(); + IMemory memory = stunt.ToObject(); + + ref int value = ref memory.Get(); value = 42; // Original value changes too :) diff --git a/src/Stunts.UnitTests/StuntExtensionsTests.cs b/src/Stunts.UnitTests/StuntExtensionsTests.cs index bed8b55a..e3593684 100644 --- a/src/Stunts.UnitTests/StuntExtensionsTests.cs +++ b/src/Stunts.UnitTests/StuntExtensionsTests.cs @@ -40,75 +40,49 @@ public void AddBehavior() } [Fact] - public void AddAnonymousBehaviorToObject() + public void AddBehaviorPreservesTheStuntType() { - object stunt = new TestStunt(); - Func method = ToString; + var stunt = new StuntReference(new TestStunt()); - var actual = stunt.AddBehavior( - (m, n) => new MethodReturn(m, "foo", null!), - m => true, - nameof(AddBehavior)) as IStunt; + // The concrete reference type flows through, so behaviors can keep being configured. + StuntReference actual = stunt.AddBehavior(new TestBehavior()); + ITestStunt value = actual.ToObject(); - Assert.NotNull(actual); Assert.Same(stunt, actual); - Assert.Single(actual!.Behaviors); + Assert.Same(stunt.ToObject(), value); + Assert.Single(stunt.Behaviors); } [Fact] - public void AddAnonymousBehaviorToNonStuntThrows() + public void AddBehaviorToNullStuntThrows() { - var stunt = new object(); - Func method = ToString; + IStunt stunt = null!; - Assert.Throws(() => stunt.AddBehavior( - (m, n) => new MethodReturn(m, "foo", null!), - m => true, - nameof(AddBehavior))); - } - - [Fact] - public void AddBehaviorToNonStuntThrows() - { - var stunt = new object(); - Func method = ToString; - - Assert.Throws(() => stunt.AddBehavior(new TestBehavior())); + Assert.Throws(() => stunt.AddBehavior(new TestBehavior())); } [Fact] public void AddBehaviorFollowsDelegateTarget() { - var stunt = new TestStunt(); - var action = (Action)Delegate.CreateDelegate(typeof(Action), stunt, typeof(TestStunt).GetMethod(nameof(TestStunt.Do))!); + var target = new TestStunt(); + var action = (Action)Delegate.CreateDelegate(typeof(Action), target, typeof(TestStunt).GetMethod(nameof(TestStunt.Do))!); + var stunt = new StuntReference(action); - var actual = action.AddBehavior(new TestBehavior()); + var actual = stunt.AddBehavior(new TestBehavior()); - Assert.Same(action, actual); - Assert.Single(stunt.Behaviors); + Assert.Same(stunt, actual); + Assert.Single(target.Behaviors); } [Fact] public void AddBehaviorOnCombinedDelegateThrows() { - var stunt = new TestStunt(); - var action = (Action)Delegate.CreateDelegate(typeof(Action), stunt, typeof(TestStunt).GetMethod(nameof(TestStunt.Do))!); + var target = new TestStunt(); + var action = (Action)Delegate.CreateDelegate(typeof(Action), target, typeof(TestStunt).GetMethod(nameof(TestStunt.Do))!); Action other = () => { }; var combined = (Action)Delegate.Combine(action, other); - Assert.Throws(() => combined.AddBehavior(new TestBehavior())); - } - - [Fact] - public void AddBehaviorToObject() - { - object stunt = new TestStunt(); - - var actual = stunt.AddBehavior(new TestBehavior()) as IStunt; - - Assert.NotNull(actual); - Assert.Same(stunt, actual); - Assert.Single(actual!.Behaviors); + Assert.Throws(() => new StuntReference(combined)); } [Fact] @@ -145,53 +119,27 @@ public void InsertBehavior() } [Fact] - public void InsertAnonymousBehaviorToObject() + public void InsertBehaviorPreservesTheStuntType() { - object stunt = new TestStunt(); - Func method = ToString; + var stunt = new StuntReference(new TestStunt()); - var actual = stunt.InsertBehavior(0, - (m, n) => new MethodReturn(m, "foo", null!), - m => true, - nameof(InsertAnonymousBehaviorToObject)) as IStunt; + StuntReference actual = stunt.InsertBehavior(0, new TestBehavior()); - Assert.NotNull(actual); Assert.Same(stunt, actual); - Assert.Single(actual!.Behaviors); - } - - [Fact] - public void InsertAnonymousBehaviorToNonStuntThrows() - { - var stunt = new object(); - Func method = ToString; - - Assert.Throws(() => stunt.InsertBehavior(0, - (m, n) => new MethodReturn(m, "foo", null!), - m => true, - nameof(InsertAnonymousBehaviorToNonStuntThrows))); + Assert.Single(stunt.Behaviors); } [Fact] - public void InsertBehaviorToNonStuntThrows() + public void InsertBehaviorToNullStuntThrows() { - var stunt = new object(); - Func method = ToString; + IStunt stunt = null!; - Assert.Throws(() => stunt.InsertBehavior(0, new TestBehavior())); + Assert.Throws(() => stunt.InsertBehavior(0, new TestBehavior())); } [Fact] - public void InsertBehaviorToObject() - { - object stunt = new TestStunt(); - - var actual = stunt.InsertBehavior(0, new TestBehavior()) as IStunt; - - Assert.NotNull(actual); - Assert.Same(stunt, actual); - Assert.Single(actual!.Behaviors); - } + public void StuntOfNonStuntThrows() + => Assert.Throws(() => new StuntReference(new object())); class TestBehavior : IStuntBehavior { @@ -203,7 +151,12 @@ public IMethodReturn Execute(IMethodInvocation invocation, ExecuteHandler next) public override string ToString() => nameof(TestBehavior); } - class TestStunt : IStunt + public interface ITestStunt + { + void Do(); + } + + class TestStunt : ITestStunt, IStunt { public IList Behaviors { get; } = new BehaviorsCollection(); diff --git a/src/Stunts/StuntExtensions.cs b/src/Stunts/StuntExtensions.cs index cc47a6a7..be68c29e 100644 --- a/src/Stunts/StuntExtensions.cs +++ b/src/Stunts/StuntExtensions.cs @@ -15,41 +15,13 @@ public static class StuntExtensions /// (invocation, next) => invocation.CreateValueReturn() | invocation.CreateExceptionReturn() | next().Invoke(invocation, next) /// invocation => true|false /// Optional friendly name for the behavior. - public static IStunt AddBehavior(this IStunt stunt, ExecuteHandler behavior, AppliesToHandler? appliesTo = null, string? name = null) - { - stunt.Behaviors.Add(new AnonymousBehavior(behavior, appliesTo, name)); - return stunt; - } - - /// - /// Adds a behavior to a stunt. - /// - /// The stunt to add the behavior to. - /// A custom behavior to apply to the stunt. - public static IStunt AddBehavior(this IStunt stunt, IStuntBehavior behavior) - { - stunt.Behaviors.Add(behavior); - return stunt; - } - - /// - /// Adds a behavior to a stunt. - /// - /// The stunt to add the behavior to. - /// (invocation, next) => invocation.CreateValueReturn() | invocation.CreateExceptionReturn() | next().Invoke(invocation, next) - /// invocation => true|false - /// Optional friendly name for the behavior. - //[EditorBrowsable(EditorBrowsableState.Advanced)] public static TStunt AddBehavior(this TStunt stunt, ExecuteHandler behavior, AppliesToHandler? appliesTo = null, string? name = null) + where TStunt : IStunt { - // We can't just add a constraint to the method signature, because - // proxies are typically generated and don't expose the IStunt interface directly. - // A delegate stunt is the bound delegate; its target is the generated instance. - if (AsStunt(stunt) is IStunt target) - target.Behaviors.Add(new AnonymousBehavior(behavior, appliesTo, name)); - else - throw new ArgumentException(nameof(stunt)); + if (stunt == null) + throw new ArgumentNullException(nameof(stunt)); + stunt.Behaviors.Add(new AnonymousBehavior(behavior, appliesTo, name)); return stunt; } @@ -58,14 +30,13 @@ public static TStunt AddBehavior(this TStunt stunt, ExecuteHandler behav /// /// The stunt to add the behavior to. /// A custom behavior to apply to the stunt. - //[EditorBrowsable(EditorBrowsableState.Advanced)] public static TStunt AddBehavior(this TStunt stunt, IStuntBehavior behavior) + where TStunt : IStunt { - if (AsStunt(stunt) is IStunt target) - target.Behaviors.Add(behavior); - else - throw new ArgumentException(nameof(stunt)); + if (stunt == null) + throw new ArgumentNullException(nameof(stunt)); + stunt.Behaviors.Add(behavior); return stunt; } @@ -78,70 +49,31 @@ public static TStunt AddBehavior(this TStunt stunt, IStuntBehavior behav /// (invocation, next) => invocation.CreateValueReturn() | invocation.CreateExceptionReturn() | next().Invoke(invocation, next) /// invocation => true|false /// Optional friendly name for the behavior. - public static IStunt InsertBehavior(this IStunt stunt, int index, ExecuteHandler behavior, AppliesToHandler? appliesTo = null, string? name = null) - { - stunt.Behaviors.Insert(index, new AnonymousBehavior(behavior, appliesTo, name)); - return stunt; - } - - /// - /// Inserts a behavior into the stunt behavior pipeline at the specified - /// index. - /// - /// The stunt to add the behavior to. - /// The index to insert the behavior at. - /// A custom behavior to apply to the stunt. - public static IStunt InsertBehavior(this IStunt stunt, int index, IStuntBehavior behavior) - { - stunt.Behaviors.Insert(index, behavior); - return stunt; - } - - /// - /// Inserts a behavior into the stunt behavior pipeline at the specified - /// index. - /// - /// The stunt to add the behavior to. - /// The index to insert the behavior at. - /// (invocation, next) => invocation.CreateValueReturn() | invocation.CreateExceptionReturn() | next().Invoke(invocation, next) - /// invocation => true|false - /// Optional friendly name for the behavior. - //[EditorBrowsable(EditorBrowsableState.Advanced)] public static TStunt InsertBehavior(this TStunt stunt, int index, ExecuteHandler behavior, AppliesToHandler? appliesTo = null, string? name = null) + where TStunt : IStunt { - if (AsStunt(stunt) is IStunt target) - target.Behaviors.Insert(index, new AnonymousBehavior(behavior, appliesTo, name)); - else - throw new ArgumentException(nameof(stunt)); + if (stunt == null) + throw new ArgumentNullException(nameof(stunt)); + stunt.Behaviors.Insert(index, new AnonymousBehavior(behavior, appliesTo, name)); return stunt; } /// - /// Inserts a behavior into the stunt behavior pipeline at the specified + /// Inserts a behavior into the stunt behavior pipeline at the specified /// index. /// /// The stunt to add the behavior to. /// The index to insert the behavior at. /// A custom behavior to apply to the stunt. - //[EditorBrowsable(EditorBrowsableState.Advanced)] public static TStunt InsertBehavior(this TStunt stunt, int index, IStuntBehavior behavior) + where TStunt : IStunt { - if (AsStunt(stunt) is IStunt target) - target.Behaviors.Insert(index, behavior); - else - throw new ArgumentException(nameof(stunt)); + if (stunt == null) + throw new ArgumentNullException(nameof(stunt)); + stunt.Behaviors.Insert(index, behavior); return stunt; } - - static IStunt? AsStunt(TStunt stunt) - { - if (stunt is IStunt target) - return target; - if (stunt is Delegate delegateStunt && delegateStunt.Target is IStunt delegateTarget) - return delegateTarget; - return null; - } } }