Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider Supporting Open Generics #287

Closed
JasonBock opened this issue Jan 9, 2024 · 2 comments
Closed

Consider Supporting Open Generics #287

JasonBock opened this issue Jan 9, 2024 · 2 comments

Comments

@JasonBock
Copy link
Owner

Describe the solution you'd like
Rocks has always forced users to close generic types. For example, if you had this:

public interface IService<T, TReturn>
{
	TReturn Service(T data);
}

You had to provide types to create the mock:

var expectations = Rock.Create<IService<int, string>>();

I know there are reasons why I forced this and didn't support open generics, but....with the new approach of using attributes on the way, I think it's worth revisiting. You'd still be able to pass in closed generics, but I think this is possible:

[RockCreate(typeof(IService<,>))]

I'll add a code example in a follow-up comment. But, I think this is possible. And the generated expectations name is cleaner to me: IServiceCreateExpectations<T, TReturn>. Furthermore, you wouldn't have to generate mock code for each closed generic; you could specify you want this, and the types could be specified when you start creating expectations. Here's a code dump of a POC (I've already verified this would pass the test :) ):

// <auto-generated/>

#nullable enable

using NUnit.Framework;

public static class Playground
{
	[Test]
	public static void Go()
	{
		var intStringExpectations = new IServiceCreateExpectations<int, string>();
		intStringExpectations.Methods.Service(3).ReturnValue("three");

		var intStringMock = intStringExpectations.Instance();
		Assert.AreEqual("three", intStringMock.Service(3));
		intStringExpectations.Verify();

		var stringIntExpectations = new IServiceCreateExpectations<string, int>();
		stringIntExpectations.Methods.Service("four").ReturnValue(4);

		var stringIntMock = stringIntExpectations.Instance();
		Assert.AreEqual(4, stringIntMock.Service("four"));
		stringIntExpectations.Verify();
	}
}


internal sealed class IServiceCreateExpectations<T, TReturn>
	: global::Rocks.Expectations.ExpectationsV4
{
#pragma warning disable CS8618

	internal sealed class Handler0
		: global::Rocks.HandlerV4<global::System.Func<T, TReturn>, TReturn>
	{
		public global::Rocks.Argument<T> @data { get; set; }
	}

#pragma warning restore CS8618

	private readonly global::System.Collections.Generic.List<global::IServiceCreateExpectations<T, TReturn>.Handler0> @handlers0 = new();

	public override void Verify()
	{
		if (this.WasInstanceInvoked)
		{
			var failures = new global::System.Collections.Generic.List<string>();

			failures.AddRange(this.Verify(handlers0));

			if (failures.Count > 0)
			{
				throw new global::Rocks.Exceptions.VerificationException(failures);
			}
		}
	}

	private sealed class RockIService
		: global::IService<T, TReturn>
	{
		public RockIService(global::IServiceCreateExpectations<T, TReturn> @expectations)
		{
			this.Expectations = @expectations;
		}

		[global::Rocks.MemberIdentifier(0, "TReturn Service(T @data)")]
		public TReturn Service(T @data)
		{
			if (this.Expectations.handlers0.Count > 0)
			{
				foreach (var @handler in this.Expectations.handlers0)
				{
					if (@handler.@data.IsValid(@data!))
					{
						@handler.CallCount++;
						var @result = @handler.Callback is not null ?
							@handler.Callback(@data!) : @handler.ReturnValue;
						return @result!;
					}
				}

				throw new global::Rocks.Exceptions.ExpectationException("No handlers match for TReturn Service(T @data)");
			}

			throw new global::Rocks.Exceptions.ExpectationException("No handlers were found for TReturn Service(T @data)");
		}

		private global::IServiceCreateExpectations<T, TReturn> Expectations { get; }
	}

	internal sealed class IServiceMethodExpectations
	{
		internal IServiceMethodExpectations(global::IServiceCreateExpectations<T, TReturn> expectations) =>
			this.Expectations = expectations;

		internal global::Rocks.AdornmentsV4<global::IServiceCreateExpectations<T, TReturn>.Handler0, global::System.Func<T, TReturn>, TReturn> Service(global::Rocks.Argument<T> @data)
		{
			global::System.ArgumentNullException.ThrowIfNull(@data);

			var handler = new global::IServiceCreateExpectations<T, TReturn>.Handler0
			{
				@data = @data,
			};

			this.Expectations.handlers0.Add(handler);
			return new(handler);
		}

		private global::IServiceCreateExpectations<T, TReturn> Expectations { get; }
	}

	internal global::IServiceCreateExpectations<T, TReturn>.IServiceMethodExpectations Methods { get; }

	internal IServiceCreateExpectations() =>
		(this.Methods) = (new(this));

	internal global::IService<T, TReturn> Instance()
	{
		if (!this.WasInstanceInvoked)
		{
			this.WasInstanceInvoked = true;
			var @mock = new RockIService(this);
			this.MockType = @mock.GetType();
			return @mock;
		}
		else
		{
			throw new global::Rocks.Exceptions.NewMockInstanceException("Can only create a new mock once.");
		}
	}
}

public interface IService<T, TReturn>
{
	TReturn Service(T data);
}
@JasonBock
Copy link
Owner Author

I should also note that I want this delivered with an 8.0.0-alpha.2 release. The FAWMN, cast-free implementation is almost complete (just changing tests around), and I want to deliver a 8.0.0-alpha.1 as soon as I can.

@JasonBock
Copy link
Owner Author

If/when I do this, update all integration tests to use open generics (and move all the attributes to the class level)

@JasonBock JasonBock modified the milestones: 8.0.0, 8.1.0 Jan 21, 2024
JasonBock added a commit that referenced this issue Mar 11, 2024
JasonBock added a commit that referenced this issue Mar 13, 2024
…od implementations, and expectations (still fixing more)
JasonBock added a commit that referenced this issue Mar 16, 2024
JasonBock added a commit that referenced this issue Mar 16, 2024
JasonBock added a commit that referenced this issue Mar 16, 2024
…ReferenceModel values, and create a GetFullyQualifiedName() to make it easier to make names
JasonBock added a commit that referenced this issue Mar 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant