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

Guard APIs #3131

Merged
merged 128 commits into from
Mar 15, 2020
Merged

Conversation

Sergio0694
Copy link
Member

@Sergio0694 Sergio0694 commented Feb 13, 2020

PR Type

What kind of change does this PR introduce?

  • Feature

What is the new behavior?

This PR introduces the Guard class, which can be used to validate method arguments in a much more streamlined manner, which is also less verbose and less error prone.

These APIs are built with three core principles in mind:

  • Being FAST. To achieve that, all the APIs have been designed to produce as little code as possible in the caller, and each single Guard API will (almost always) be inlined. Furthermore, specialized methods are generated with T4 templates to achieve the most efficient assembly code possible when working with common types (eg. primitive numeric types).
  • Being HELPFUL. Each Guard API produces a detailed exception message that clearly specifies what went wrong, along with additional info (eg. current variable values), when applicable.
  • Being INTUITIVE. To achieve this, all the Guard APIs have expressive and self-explanatory names that make it immediately clear what each API is supposed to do. This shifts the burden of writing checks away from the developers, letting them express conditions using natural language.

Here is a sample of how these APIs greatly reduce the complexity of code and make it easier to write and to follow, while also making it less error prone:

// Before
if (array is null) throw new ArgumentNullException("The array must not be null");
if (array.Length >= 10) throw new ArgumentException($"The array must have less than 10 items, had a size of {array.Length}");
if (index < 0 || index >= array.Length) throw new ArgumentOutOfRangeException($"The index must be in the [0, {array.Length}) range, was {index}");

// After
Guard.IsNotNull(array, nameof(array));
Guard.HasSizeLessThan(array, 10, nameof(array));
Guard.IsInRangeFor(index, array, nameof(index));

Below is a breakthrough of most of the new types and APIs added in this PR (there are more):

Click to expand!
namespace Microsoft.Toolkit.Diagnostics
{
    public static class Guard
    {
        public static void IsNull<T>(T? value, string name) where T : class;
        public static void IsNull<T>(T? value, string name) where T : struct;
        public static void IsNotNull<T>(T? value, string name) where T : class;
        public static void IsNotNull<T>(T? value, string name) where T : struct;
        public static void IsOfType(object value, string name);
        public static void IsOfType(object value, Type type, string name);
        public static void IsAssignableToType<T>(object value, string name);
        public static void IsAssignableToType(object value, Type type, string name);
        public static void IsEqualTo<T>(T value, T target, string name) where T : notnull, IEquatable<T>;
        public static void IsNotEqualTo<T>(T value, T target, string name) where T : notnull, IEquatable<T>;
        public static void IsBitwiseEqualTo<T>(T value, T target, string name) where T : unmanaged;
        public static void IsReferenceEqualTo<T>(T value, T target, string name) where T : class;
        public static void IsReferenceNotEqualTo<T>(T value, T target, string name) where T : class;
        public static void IsTrue(bool value, string name);
        public static void IsFalse(bool value, string name);
        public static void IsLessThan<T>(T value, T max, string name) where T : notnull, IComparable<T>;
        public static void IsLessThanOrEqualTo<T>(T value, T maximum, string name) where T : notnull, IComparable<T>;
        public static void IsGreaterThan<T>(T value, T minimum, string name) where T : notnull, IComparable<T>;
        public static void IsGreaterThanOrEqualTo<T>(T value, T minimum, string name) where T : notnull, IComparable<T>;
        public static void IsInRange<T>(T value, T minimum, T maximum, string name) where T : notnull, IComparable<T>;
        public static void IsNotInRange<T>(T value, T minimum, T maximum, string name) where T : notnull, IComparable<T>;
        public static void IsBetween<T>(T value, T minimum, T maximum, string name) where T : notnull, IComparable<T>;
        public static void IsNotBetween<T>(T value, T minimum, T maximum, string name) where T : notnull, IComparable<T>;
        public static void IsBetweenOrEqualTo<T>(T value, T minimum, T maximum, string name) where T : notnull, IComparable<T>;
        public static void IsNotBetweenOrEqualTo<T>(T value, T minimum, T maximum, string name);
        public static void IsNullOrEmpty(string? text, string name);
        public static void IsNotNullOrEmpty(string? text, string name);
        public static void IsNullOrWhitespace(string? text, string name);
        public static void IsNotNullOrWhitespace(string? text, string name);
        public static void IsEmpty(string text, string name);
        public static void IsNotEmpty(string text, string name);
        public static void IsWhitespace(string text, string name);
        public static void IsNotWhitespace(string text, string name);
        public static void HasSizeEqualTo(string text, int size, string name);
        public static void HasSizeNotEqualTo(string text, int size, string name);
        public static void HasSizeAtLeast(string text, int size, string name);
        public static void HasSizeAtLeastOrEqualTo(string text, int size, string name);
        public static void HasSizeLessThan(string text, int size, string name);
        public static void HasSizeLessThanOrEqualTo(string text, int size, string name);
        public static void IsEmpty<T>(T[] array, string name);
        public static void IsNotEmpty<T>(T[] array, string name);
        public static void HasSizeEqualTo<T>(T[] array, int size, string name);
        public static void HasSizeNotEqualTo<T>(T[] array, int size, string name);
        public static void HasSizeAtLeast<T>(T[] array, int size, string name);
        public static void HasSizeAtLeastOrEqualTo<T>(T[] array, int size, string name);
        public static void HasSizeLessThan<T>(T[] array, int size, string name);
        public static void HasSizeLessThanOrEqualTo<T>(T[] array, int size, string name);
        public static void HasSizeEqualTo<T>(T[] source, T[] destination, string name);
        public static void HasSizeLessThanOrEqualTo<T>(T[] source, T[] destination, string name);
        public static void IsEmpty<T>(IEnumerable<T> enumerable, string name);
        public static void IsNotEmpty<T>(IEnumerable<T> enumerable, string name);
        public static void HasSizeEqualTo<T>(IEnumerable<T> enumerable, int size, string name);
        public static void HasSizeNotEqualTo<T>(IEnumerable<T> enumerable, int size, string name);
        public static void HasSizeAtLeast<T>(IEnumerable<T> enumerable, int size, string name);
        public static void HasSizeAtLeastOrEqualTo<T>(IEnumerable<T> enumerable, int size, string name);
        public static void HasSizeLessThan<T>(IEnumerable<T> enumerable, int size, string name);
        public static void HasSizeLessThanOrEqualTo<T>(IEnumerable<T> enumerable, int size, string name);
        public static void IsEmpty<T>(ReadOnlySpan<T> span, string name);
        public static void IsNotEmpty<T>(ReadOnlySpan<T> span, string name);
        public static void HasSizeEqualTo<T>(ReadOnlySpan<T> span, int size, string name);
        public static void HasSizeNotEqualTo<T>(ReadOnlySpan<T> span, int size, string name);
        public static void HasSizeAtLeast<T>(ReadOnlySpan<T> span, int size, string name);
        public static void HasSizeAtLeastOrEqualTo<T>(ReadOnlySpan<T> span, int size, string name);
        public static void HasSizeLessThan<T>(ReadOnlySpan<T> span, int size, string name);
        public static void HasSizeLessThanOrEqualTo<T>(ReadOnlySpan<T> span, int size, string name);
        public static void HasSizeEqualTo<T>(ReadOnlySpan<T> source, Span<T> destination, string name);
        public static void HasSizeLessThanOrEqualTo<T>(ReadOnlySpan<T> source, Span<T> destination, string name);
        public static void IsEmpty<T>(ReadOnlyMemory<T> memory, string name);
        public static void IsNotEmpty<T>(ReadOnlyMemory<T> memory, string name);
        public static void HasSizeEqualTo<T>(ReadOnlyMemory<T> memory, int size, string name);
        public static void HasSizeNotEqualTo<T>(ReadOnlyMemory<T> memory, int size, string name);
        public static void HasSizeAtLeast<T>(ReadOnlyMemory<T> memory, int size, string name);
        public static void HasSizeAtLeastOrEqualTo<T>(ReadOnlyMemory<T> memory, int size, string name);
        public static void HasSizeLessThan<T>(ReadOnlyMemory<T> memory, int size, string name);
        public static void HasSizeLessThanOrEqualTo<T>(ReadOnlyMemory<T> memory, int size, string name);
        public static void HasSizeEqualTo<T>(ReadOnlyMemory<T> source, Memory<T> destination, string name);
        public static void HasSizeLessThanOrEqualTo<T>(ReadOnlyMemory<T> source, Memory<T> destination, string name);
        public static void CanRead(Stream stream, string name);
        public static void CanWrite(Stream stream, string name);
        public static void CanSeek(Stream stream, string name);
        public static void IsAtStartPosition(Stream stream, string name);
        public static void IsCompleted(Task task, string name);
        public static void IsCompletedSuccessfully(Task task, string name);
        public static void IsFaulted(Task task, string name);
        public static void IsCanceled(Task task, string name);
        public static void HasStatus(Task task, TaskStatus status, string name);
    }
}

There are also a couple of new extension methods being introduced in this PR, which are used internally. Since they were pretty general and easily reusable, they have been marked as public:

namespace Microsoft.Toolkit.Extensions
{
    public static string ToHexString<T>(this T value) where T : unmanaged;
    public static string ToTypeString(this Type type);
}

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tested code with current supported SDKs
  • Pull Request has been submitted to the documentation repository instructions. Link: #302
  • Sample in sample app has been added / updated (for bug fixes / features)
  • Tests for the changes have been added (for bug fixes / features) (if applicable)
  • Header has been added to all new source files (run build/UpdateHeaders.bat)
  • Contains NO breaking changes

Other information

This PR is a follow up from a conversation with @michael-hawker over on the UWP Community Discord server. The DebugClass is missing for now, but since it should be exactly identical to the Guard class, just with the added [Conditional("DEBUG")] attribute, I was thinking we could probably use a T4 template (or some similar technique) to just auto-generate this class by copying the Guard code and adding that single attribute to the new class. I'm not personally familiar with setting up a T4 template, so I would need some help to get that figured out. I'll work on the Guard class in the meantime 👍

@ghost
Copy link

ghost commented Feb 13, 2020

Thanks Sergio0694 for opening a Pull Request! The reviewers will test the PR and highlight if there is any conflict or changes required. If the PR is approved we will proceed to merge the pull request 🙌

@ghost ghost assigned michael-hawker Feb 13, 2020
@ghost
Copy link

ghost commented Mar 13, 2020

This PR has been marked as "needs attention 👋" and awaiting a response from the team.

@michael-hawker
Copy link
Member

Note to self: Squash this commit as per @azchohfi's recommendation.

@Sergio0694 thanks for this amazing addition! I'm just going to do a quick final play locally with it in the morning and then will merge the PR!

@michael-hawker
Copy link
Member

@Sergio0694 seeing 4 test cases fail?

IsCloseToFloat_Fail
IsCloseToInt_Fail
IsNull_ClassFail
IsNull_StructFail

@Sergio0694
Copy link
Member Author

@michael-hawker Good catch! Fixed in 51b45e2 and 244da99.

  • IsCloseToFloat_Fail was failing due to a rounding error, float can't actually tell the difference between float.MaxValue and float.MaxValue - 10. Changed that to float.MaxValue / 2.
  • IsCloseToInt_Fail was failing because I had accidentally used int as the delta, which can't be implicitly cast to uint, so the compiler was picking up the IsCloseTo overload with float parameters instead. And those were again failing due to insufficient precision. Fixed that by changing the delta type to uint in the tests.
  • IsNull_ClassFail and IsNull_StructFail were indeed failing because I was watching for ArgumentNullException in the tests, as opposed to just ArgumentException.

All are passing now 😄

Copy link
Member

@michael-hawker michael-hawker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

@michael-hawker michael-hawker merged commit ddf8ee0 into CommunityToolkit:master Mar 15, 2020
@Sergio0694
Copy link
Member Author

Wo-hoo! 😄 🚀

Thank you so much @michael-hawker - so happy to have my first feature PR merged in!
Can't wait for other developers to give the new APIs a try once the new update goes live! 🎉

@Sergio0694 Sergio0694 deleted the feature/guard-apis branch March 15, 2020 01:51
@michael-hawker michael-hawker mentioned this pull request Mar 27, 2020
36 tasks
@Sergio0694 Sergio0694 mentioned this pull request Apr 17, 2020
8 tasks
@Sergio0694 Sergio0694 mentioned this pull request Jun 4, 2020
7 tasks
@Kyaa-dost Kyaa-dost added improvements ✨ and removed feature 💡 for-review 📖 To evaluate and validate the Issues or PR needs attention 👋 labels Jun 9, 2020
ghost pushed a commit that referenced this pull request Oct 2, 2020
## Follow up for #3131 

<!-- Add a brief overview here of the feature/bug & fix. -->
The `Type.ToTypeString()` introduced with the `Guard` API lacks support for nested types, pointer types and byref types.

## PR Type
What kind of change does this PR introduce?
<!-- Please uncomment one or more that apply to this PR. -->

<!-- - Bugfix -->
<!-- - Feature -->
<!-- - Code style update (formatting) -->
<!-- - Refactoring (no functional changes, no api changes) -->
<!-- - Build or CI related changes -->
<!-- - Documentation content changes -->
<!-- - Sample app changes -->
<!-- - Other... Please describe: -->


## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
The extension doesn't work properly when used with nested, pointer or byref types.
It will either crash or just return the wrong formatted type (eg. the declaring type instead of the nested type).

## What is the new behavior?
<!-- Describe how was this issue resolved or changed? -->
The extension now works correctly with all sorts of nested, pointer and byref types 🎉

## PR Checklist

Please check if your PR fulfills the following requirements:

- [X] Tested code with current [supported SDKs](../readme.md#supported)
- [ ] ~~Pull Request has been submitted to the documentation repository [instructions](..\contributing.md#docs).~~
- [ ] ~~Sample in sample app has been added / updated (for bug fixes / features)~~
    - [ ] ~~Icon has been created (if new sample) following the [Thumbnail Style Guide and templates](https:/windows-toolkit/WindowsCommunityToolkit-design-assets)~~
- [X] Tests for the changes have been added (for bug fixes / features) (if applicable)
- [X] Header has been added to all new source files (run *build/UpdateHeaders.bat*)
- [X] Contains **NO** breaking changes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants