Last active
October 8, 2015 01:35
-
-
Save mowensoft/6485e6b37601d97a52c9 to your computer and use it in GitHub Desktop.
Guard clauses based on https://github.com/danielwertheim/Ensure.That
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using System.Diagnostics; | |
using System.IO; | |
using System.Linq; | |
using System.Text.RegularExpressions; | |
namespace EnsureThat | |
{ | |
public static class Ensure | |
{ | |
#region That method | |
[DebuggerStepThrough] | |
public static Param<T> That<T>(T value, string name = Param.DefaultName) | |
{ | |
return new Param<T>(value, name); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam ThatTypeFor<T>(T value, string name = Param.DefaultName) | |
{ | |
return new TypeParam(name, value.GetType()); | |
} | |
#endregion | |
#region Bool Extensions | |
[DebuggerStepThrough] | |
public static Param<bool> IsTrue(this Param<bool> param) | |
{ | |
if (!param.Value) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotTrue); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<bool> IsFalse(this Param<bool> param) | |
{ | |
if (param.Value) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotFalse); | |
} | |
return param; | |
} | |
#endregion | |
#region Collection Extensions | |
[DebuggerStepThrough] | |
public static Param<T> HasItems<T>(this Param<T> param) where T : class, ICollection | |
{ | |
if (param.Value == null || param.Value.Count < 1) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsEmptyCollection); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<Collection<T>> HasItems<T>(this Param<Collection<T>> param) | |
{ | |
if (param.Value == null || param.Value.Count < 1) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsEmptyCollection); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<ICollection<T>> HasItems<T>(this Param<ICollection<T>> param) | |
{ | |
if (param.Value == null || param.Value.Count < 1) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsEmptyCollection); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T[]> HasItems<T>(this Param<T[]> param) | |
{ | |
if (param.Value == null || param.Value.Length < 1) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsEmptyCollection); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<List<T>> HasItems<T>(this Param<List<T>> param) | |
{ | |
if (param.Value == null || param.Value.Count < 1) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsEmptyCollection); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<IList<T>> HasItems<T>(this Param<IList<T>> param) | |
{ | |
if (param.Value == null || param.Value.Count < 1) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsEmptyCollection); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<IDictionary<TKey, TValue>> HasItems<TKey, TValue>(this Param<IDictionary<TKey, TValue>> param) | |
{ | |
if (param.Value == null || param.Value.Count < 1) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsEmptyCollection); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T[]> SizeIs<T>(this Param<T[]> param, int expectedSize) | |
{ | |
if (param.Value.Length != expectedSize) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.SizeIsWrong.Inject(expectedSize)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<IList<T>> SizeIs<T>(this Param<IList<T>> param, int expectedSize) | |
{ | |
if (param.Value.Count != expectedSize) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.SizeIsWrong.Inject(expectedSize)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<List<T>> SizeIs<T>(this Param<List<T>> param, int expectedSize) | |
{ | |
if (param.Value.Count != expectedSize) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.SizeIsWrong.Inject(expectedSize)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<ICollection<T>> SizeIs<T>(this Param<ICollection<T>> param, int expectedSize) | |
{ | |
if (param.Value.Count != expectedSize) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.SizeIsWrong.Inject(expectedSize)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<Collection<T>> SizeIs<T>(this Param<Collection<T>> param, int expectedSize) | |
{ | |
if (param.Value.Count != expectedSize) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.SizeIsWrong.Inject(expectedSize)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<IDictionary<TKey, TValue>> ContainsKey<TKey, TValue>(this Param<IDictionary<TKey, TValue>> param, TKey key) | |
{ | |
if (!param.Value.ContainsKey(key)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.ContainsKey.Inject(key)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<Dictionary<TKey, TValue>> ContainsKey<TKey, TValue>(this Param<Dictionary<TKey, TValue>> param, TKey key) | |
{ | |
if (!param.Value.ContainsKey(key)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.ContainsKey.Inject(key)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<IList<T>> Any<T>(this Param<IList<T>> param, Func<T, bool> predicate) | |
{ | |
if (!param.Value.Any(predicate)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.AnyPredicateYieldedNone); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<List<T>> Any<T>(this Param<List<T>> param, Func<T, bool> predicate) | |
{ | |
if (!param.Value.Any(predicate)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.AnyPredicateYieldedNone); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<ICollection<T>> Any<T>(this Param<ICollection<T>> param, Func<T, bool> predicate) | |
{ | |
if (!param.Value.Any(predicate)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.AnyPredicateYieldedNone); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<Collection<T>> Any<T>(this Param<Collection<T>> param, Func<T, bool> predicate) | |
{ | |
if (!param.Value.Any(predicate)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.AnyPredicateYieldedNone); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T[]> Any<T>(this Param<T[]> param, Func<T, bool> predicate) | |
{ | |
if (!param.Value.Any(predicate)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.AnyPredicateYieldedNone); | |
} | |
return param; | |
} | |
#endregion | |
#region Comparable Extensions | |
public static bool IsLessThan<T>(this IComparable<T> x, T y) | |
{ | |
return x.CompareTo(y) < 0; | |
} | |
public static bool IsGreaterThan<T>(this IComparable<T> x, T y) | |
{ | |
return x.CompareTo(y) > 0; | |
} | |
public static bool IsEqualTo<T>(this IComparable<T> x, T y) | |
{ | |
return x.CompareTo(y) == 0; | |
} | |
[DebuggerStepThrough] | |
public static Param<T> IsEqualTo<T>(this Param<T> param, T expected) where T : struct, IComparable<T> | |
{ | |
if (!param.Value.IsEqualTo(expected)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsFailed.Inject(param.Value, expected)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T> IsNotEqualTo<T>(this Param<T> param, T expected) where T : struct, IComparable<T> | |
{ | |
if (param.Value.IsEqualTo(expected)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotFailed.Inject(param.Value, expected)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T> IsLessThan<T>(this Param<T> param, T limit) where T : struct, IComparable<T> | |
{ | |
if (!param.Value.IsLessThan(limit)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotLt.Inject(param.Value, limit)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T> IsLessThanOrEqualTo<T>(this Param<T> param, T limit) where T : struct, IComparable<T> | |
{ | |
if (param.Value.IsGreaterThan(limit)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotLte.Inject(param.Value, limit)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T> IsGreaterThan<T>(this Param<T> param, T limit) where T : struct, IComparable<T> | |
{ | |
if (!param.Value.IsGreaterThan(limit)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotGt.Inject(param.Value, limit)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T> IsGreaterThanOrEqualTo<T>(this Param<T> param, T limit) where T : struct, IComparable<T> | |
{ | |
if (param.Value.IsLessThan(limit)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotGte.Inject(param.Value, limit)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<T> IsInRange<T>(this Param<T> param, T min, T max) where T : struct, IComparable<T> | |
{ | |
if (param.Value.IsLessThan(min)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotInRangeTooLow.Inject(param.Value, min)); | |
} | |
if (param.Value.IsGreaterThan(max)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotInRangeTooHigh.Inject(param.Value, max)); | |
} | |
return param; | |
} | |
#endregion | |
#region Guid Extensions | |
public static Param<Guid> IsNotEmpty(this Param<Guid> param) | |
{ | |
if (param.Value.Equals(Guid.Empty)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotGuid.Inject(param.Value)); | |
} | |
return param; | |
} | |
#endregion | |
#region Nullable Value Type Extensions | |
[DebuggerStepThrough] | |
public static Param<T?> IsNotNull<T>(this Param<T?> param) where T : struct | |
{ | |
if (param == null || !param.Value.HasValue) | |
{ | |
throw CreateForNullParameterValidation(param, ExceptionMessages.IsNotNull); | |
} | |
return param; | |
} | |
#endregion | |
#region Object Extensions | |
[DebuggerStepThrough] | |
public static Param<T> IsNotNull<T>(this Param<T> param) where T : class | |
{ | |
if (param.Value == null) | |
{ | |
throw CreateForNullParameterValidation(param, ExceptionMessages.IsNotNull); | |
} | |
return param; | |
} | |
#endregion | |
#region String Extensions | |
[DebuggerStepThrough] | |
public static Param<string> IsNotNullOrEmpty(this Param<string> param) | |
{ | |
if (string.IsNullOrEmpty(param.Value)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotNullOrEmpty); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> IsNotNullOrWhiteSpace(this Param<string> param) | |
{ | |
if (string.IsNullOrWhiteSpace(param.Value)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotNullOrEmpty); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> HasLengthBetween(this Param<string> param, int minLength, int maxLength) | |
{ | |
if (string.IsNullOrEmpty(param.Value)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotNullOrEmpty); | |
} | |
var length = param.Value.Length; | |
if (length < minLength) | |
{ | |
throw CreateForParameterValidation( | |
param, | |
ExceptionMessages.IsNotInRangeTooShort.Inject(minLength, maxLength, length)); | |
} | |
if (length > maxLength) | |
{ | |
throw CreateForParameterValidation( | |
param, | |
ExceptionMessages.IsNotInRangeTooLong.Inject(minLength, maxLength, length)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> Matches(this Param<string> param, string pattern) | |
{ | |
return Matches(param, new Regex(pattern)); | |
} | |
[DebuggerStepThrough] | |
public static Param<string> Matches(this Param<string> param, Regex regex) | |
{ | |
if (!regex.IsMatch(param.Value)) | |
{ | |
throw CreateForParameterValidation( | |
param, | |
ExceptionMessages.NoMatch.Inject(param.Value, regex)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> FileExists(this Param<string> param) | |
{ | |
if (string.IsNullOrEmpty(param.Value)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotNullOrEmpty); | |
} | |
if (!File.Exists(param.Value)) | |
{ | |
throw CreateForParameterValidation( | |
param, | |
ExceptionMessages.FileDoesNotExist.Inject(param.Value)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> DirectoryExists(this Param<string> param) | |
{ | |
if (string.IsNullOrEmpty(param.Value)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotNullOrEmpty); | |
} | |
if (!Directory.Exists(param.Value)) | |
{ | |
throw CreateForParameterValidation( | |
param, | |
ExceptionMessages.DirectoryDoesNotExist.Inject(param.Value)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> IsGuid(this Param<string> param) | |
{ | |
Guid guid; | |
if (!Guid.TryParse(param.Value, out guid)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotGuid.Inject(param.Value)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> IsNumber(this Param<string> param) | |
{ | |
long number; | |
if (long.TryParse(param.Value, out number)) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotNumber.Inject(param.Value)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> IsEqualTo(this Param<string> param, string expected, StringComparison? comparison = null) | |
{ | |
if (!StringEquals(param.Value, expected, comparison)) | |
{ | |
throw CreateForParameterValidation( | |
param, | |
ExceptionMessages.IsFailed.Inject(param.Value, expected)); | |
} | |
return param; | |
} | |
[DebuggerStepThrough] | |
public static Param<string> IsNotEqualTo(this Param<string> param, string expected, StringComparison? comparison = null) | |
{ | |
if (StringEquals(param.Value, expected, comparison)) | |
{ | |
throw CreateForParameterValidation( | |
param, | |
ExceptionMessages.IsFailed.Inject(param.Value, expected)); | |
} | |
return param; | |
} | |
private static bool StringEquals(string x, string y, StringComparison? comparison = null) | |
{ | |
return comparison.HasValue | |
? string.Equals(x, y, comparison.Value) | |
: string.Equals(x, y); | |
} | |
public static string Inject(this string format, params object[] args) | |
{ | |
return string.Format(format, args); | |
} | |
#endregion | |
#region Type Extensions | |
private static class PrimitiveTypes | |
{ | |
internal static readonly Type IntType = typeof(int); | |
internal static readonly Type LongType = typeof(long); | |
internal static readonly Type ShortType = typeof(short); | |
internal static readonly Type DecimalType = typeof(decimal); | |
internal static readonly Type DoubleType = typeof(double); | |
internal static readonly Type FloatType = typeof(float); | |
internal static readonly Type BoolType = typeof(bool); | |
internal static readonly Type DateTimeType = typeof(DateTime); | |
internal static readonly Type StringType = typeof(string); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsInt(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.IntType); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsLong(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.LongType); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsShort(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.ShortType); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsFloat(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.FloatType); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsDecimal(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.DecimalType); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsDouble(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.DoubleType); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsBool(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.BoolType); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsDateTime(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.DateTimeType); | |
} | |
[DebuggerStepThrough] | |
public static TypeParam IsString(this TypeParam param) | |
{ | |
return IsOfType(param, PrimitiveTypes.StringType); | |
} | |
[DebuggerStepThrough] | |
private static TypeParam IsOfType(TypeParam param, Type type) | |
{ | |
if (param.Type != type) | |
{ | |
throw CreateForParameterValidation(param, ExceptionMessages.IsNotOfType.Inject(param.Type.FullName)); | |
} | |
return param; | |
} | |
#endregion | |
#region Fluent Extensions | |
[DebuggerStepThrough] | |
public static Param<T> And<T>(this Param<T> param) | |
{ | |
return param; | |
} | |
#endregion | |
#region Exception Factory | |
private static ArgumentException CreateForParameterValidation(Param param, string message) | |
{ | |
return new ArgumentException( | |
param.Message == null | |
? message | |
: string.Concat(message, Environment.NewLine, param.Message()), | |
param.Name); | |
} | |
private static ArgumentNullException CreateForNullParameterValidation(Param param, string message) | |
{ | |
return new ArgumentNullException( | |
param.Name, | |
param.Message == null | |
? message | |
: string.Concat(message, Environment.NewLine, param.Message())); | |
} | |
private static InvalidOperationException CreateForInvalidOperation(Param param, string message) | |
{ | |
var exceptionMessage = string.Format(message, param.Name); | |
return new InvalidOperationException( | |
param.Message == null | |
? exceptionMessage | |
: string.Concat(exceptionMessage, Environment.NewLine, param.Message())); | |
} | |
#endregion | |
#region Exception Messages | |
private static class ExceptionMessages | |
{ | |
public static readonly string IsNotTrue = "Expected an expression that evaluates to true."; | |
public static readonly string IsNotFalse = "Expected an expression that evaluates to false."; | |
public static readonly string IsEmptyCollection = "Empty collection is not allowed."; | |
public static readonly string SizeIsWrong = "Expected size '{0}' but found '{1}'."; | |
public static readonly string IsFailed = "Value '{0}' is not '{1}'."; | |
public static readonly string IsNotFailed = "Value '{0}' is '{1}', which was not expected."; | |
public static readonly string IsNotLt = "value '{0}' is not lower than limit '{1}'."; | |
public static readonly string IsNotLte = "value '{0}' is not lower than or equal to limit '{1}'."; | |
public static readonly string IsNotGt = "value '{0}' is not greater than limit '{1}'."; | |
public static readonly string IsNotGte = "value '{0}' is not greater than or equal to limit '{1}'."; | |
public static readonly string IsNotInRangeTooLow = "value '{0}' is < min '{1}'."; | |
public static readonly string IsNotInRangeTooHigh = "value '{0}' is > max '{1}'."; | |
public static readonly string IsNotNull = "Value can not be null."; | |
public static readonly string IsNotNullOrWhiteSpace = "The string can't be left empty, null or consist of only whitespaces."; | |
public static readonly string IsNotNullOrEmpty = "The string can't be null or empty."; | |
public static readonly string IsNotInRangeTooShort = "The string is not long enough. Must be between '{0}' and '{1}' but was '{2}' characters long."; | |
public static readonly string IsNotInRangeTooLong = "The string is too long. Must be between '{0}' and '{1}'. Must be between '{0}' and '{1}' but was '{2}' characters long."; | |
public static readonly string NoMatch = "value '{0}' does not match '{1}'"; | |
public static readonly string IsNotOfType = "The param is not of expected type: '{0}'."; | |
public static readonly string IsNotClassWasNull = "The param was expected to be a class, but was NULL."; | |
public static readonly string IsNotClass = "The param was expected to be a class, but was type of: '{0}'."; | |
public static readonly string InvalidOperationException = "Could not perform operation due to invalid state of '{0}'."; | |
public static readonly string IsEmptyGuid = "Empty Guid is not allowed."; | |
public static readonly string ContainsKey = "Key '{0}' does not exist."; | |
public static readonly string AnyPredicateYieldedNone = "The predicate did not match any elements."; | |
public static readonly string IsNotGuid = "Value '{0}' is not a valid GUID."; | |
public static readonly string IsNotNumber = "Value '{0}' is not a valid number."; | |
public static readonly string FileDoesNotExist = "The specified file '{0}' does not exist."; | |
public static readonly string DirectoryDoesNotExist = "The specified directory '{0}' does not exist."; | |
} | |
#endregion | |
} | |
#region Param class | |
public abstract class Param | |
{ | |
public const string DefaultName = ""; | |
public Func<string> Message { get; protected set; } | |
public string Name { get; protected set; } | |
protected Param(string name, Func<string> message = null) | |
{ | |
Name = name; | |
Message = message; | |
} | |
} | |
public class Param<T> : Param | |
{ | |
public T Value { get; protected set; } | |
public Param(T value, string name = "", Func<string> message = null) | |
: base(name, message) | |
{ | |
Value = value; | |
Name = name; | |
} | |
public Param<T> WithCustomMessage(Func<string> message) | |
{ | |
Message = message; | |
return this; | |
} | |
} | |
#endregion | |
#region TypeParam class | |
public class TypeParam : Param | |
{ | |
public TypeParam(string name, Type type) | |
: base(name) | |
{ | |
Type = type; | |
} | |
public Type Type { get; private set; } | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment