Add contains, exists, toArray
- Update docs
This commit is contained in:
parent
57af645d87
commit
fad428a4e4
32
README.md
32
README.md
@ -6,26 +6,32 @@ This project contains PHP utility classes whose functionality is inspired by the
|
||||
|
||||
This early-stage library currently provides two classes, both of which are designed to wrap values and indicate the state of the action that produced them. `Option<T>` represents a variable that may or may not have a value. `Result<TOK, TError>` represents the result of an action; the "ok" and "error" states both provide a value.
|
||||
|
||||
| | `Option<T>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>Replaces exception-based error handling |
|
||||
|----------------------------------------------------|--------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
|
||||
| **Creating** | `::Some(T)` for Some<br>`::None()` for None<br>`::of($value)` _None if `null`_ | `::OK(TOK)` for OK<br>`::Error(TError)` for Error |
|
||||
| **Querying** | `->isSome(): bool`<br>`->isNone(): bool` | `->isOK(): bool`<br>`->isError(): bool` |
|
||||
| **Reading**<br>_throws if called on missing value_ | `->get(): T` | `->getOK(): TOK`<br>`->getError(): TError` |
|
||||
| **Transforming**<br>_still `Option` or `Result`_ | `->map(callable(T): U): U` | `->map(callable(TOK): U): U`<br>`->mapError(callable(TError): U): U` |
|
||||
| **Iterating** | `->iter(callable(T): void): void` | `->iter(callable(TOK): void): void` |
|
||||
| **Inspecting**<br>_returns the original instance_ | `->tap(callable(Option<T>): void): void` | `->tap(callable(Result<TOK, TError>): void): void` |
|
||||
| **Continued Processing** | `->bind(callable(T): Option<TBound>): Option<TBound>` | `->bind(callable(TOK): Result<TBoundOK, TError>): Result<TBoundOK, TError>` |
|
||||
| | `Option<T>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>Replaces exception-based error handling |
|
||||
|---------------------------------------------------|-------------------------------------------------------|-----------------------------------------------------------------------------|
|
||||
| **Creating** | `::Some(T)` for Some | `::OK(TOK)` for OK |
|
||||
| | `::None()` for None | `::Error(TError)` for Error |
|
||||
| | `::of($value)` _None if `null`_ | |
|
||||
| **Querying** | `->isSome(): bool` | `->isOK(): bool` |
|
||||
| | `->isNone(): bool` | `->isError(): bool` |
|
||||
| | `->contains(T, $strict = true): bool` | `->contains(TOK, $strict = true): bool` |
|
||||
| | `->exists(callable(T): bool): bool` | `->exists(callable(TOK): bool): bool` |
|
||||
| **Reading**<br> | `->get(): T` | `->getOK(): TOK` |
|
||||
| _all throw if called on missing value_ | | `->getError(): TError` |
|
||||
| **Transforming**<br> | `->map(callable(T): TMapped): Option<TMapped>` | `->map(callable(TOK): TMapped): Result<TMapped, TError>` |
|
||||
| _all still `Option` or `Result`_ | | `->mapError(callable(TError): TMapped): Result<TOK, TMapped>` |
|
||||
| **Iterating** | `->iter(callable(T): void): void` | `->iter(callable(TOK): void): void` |
|
||||
| **Inspecting**<br>_returns the original instance_ | `->tap(callable(Option<T>): void): Option<T>` | `->tap(callable(Result<TOK, TError>): void): Result<TOK, TError>` |
|
||||
| **Continued Processing** | `->bind(callable(T): Option<TBound>): Option<TBound>` | `->bind(callable(TOK): Result<TBoundOK, TError>): Result<TBoundOK, TError>` |
|
||||
| **Changing Types** | `->toArray(): T[]` | `->toArray(): TOK[]` |
|
||||
| | | `->toOption(): Option<TOK>` |
|
||||
|
||||
In addition to this, `Option<T>` provides:
|
||||
- `->getOrDefault(T)` will return the Some value if it exists or the given default if the option is None.
|
||||
- `->getOrCall(callable(): mixed)` will call the given function if the option is None. That function may return a value, or may be `void` or `never`.
|
||||
- `->getOrThrow(callable(): Exception)` will return the Some value if it exists, or throw the exception returned by the function if the option is None.
|
||||
- `->filter(callable(T): bool)` will compare a Some value against the callable, and if it returns `true`, will remain Some; if it returns `false`, the value will become None.
|
||||
- `->is(T, $strict = true)` will return `true` if the option is Some and the value matches. Strict equality (the default) uses `===` for the comparison; if strict is set to `false`, the comparison will use `==` instead.
|
||||
- `->unwrap()` will return `null` for None options and the value for Some options.
|
||||
|
||||
`Result<TOK, TError>` also provides:
|
||||
- `toOption()` will transform an OK result to a Some option, and an Error result to a None option.
|
||||
|
||||
Finally, we would be remiss to not acknowledge some really cool prior art in this area - the [PhpOption](https://github.com/schmittjoh/php-option) project. `Option::of` recognizes their options and converts them properly, and `Option<T>` instances have a `->toPhpOption()` method that will convert these back into PhpOption's `Some<T>` and `None` instances. There is also a [ResultType](https://github.com/GrahamCampbell/Result-Type) project from the same team, though this project's result does not (yet) have any conversion methods for it.
|
||||
|
||||
## The Inspiration
|
||||
|
4
composer.lock
generated
4
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9c9d70d95d369f37fa95a10637a56a58",
|
||||
"content-hash": "9c0a6b77d5d66ee91ab9946d5f5e2107",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
@ -1721,7 +1721,7 @@
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8"
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace BitBadger\InspiredByFSharp;
|
||||
|
||||
use Error;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
@ -84,15 +85,26 @@ readonly class Option
|
||||
/**
|
||||
* Get the value, or return the value of a callable function
|
||||
*
|
||||
* @template U The return type of the callable provided
|
||||
* @param callable(): U $f The callable function to use for `None` options
|
||||
* @return T|mixed The value if `Some`, the result of the callable if `None`
|
||||
* @template TCalled The return type of the callable provided
|
||||
* @param callable(): TCalled $f The callable function to use for `None` options
|
||||
* @return T|TCalled The value if `Some`, the result of the callable if `None`
|
||||
*/
|
||||
public function getOrCall(callable $f): mixed
|
||||
{
|
||||
return $this->value ?? $f();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value, or throw the
|
||||
* @param callable(): Exception $exFunc A function to construct the exception to throw
|
||||
* @return T The value of the option if `Some`
|
||||
* @throws Exception If the option is `None`
|
||||
*/
|
||||
public function getOrThrow(callable $exFunc): mixed
|
||||
{
|
||||
return $this->value ?? throw $exFunc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a function to this option (railway processing)
|
||||
*
|
||||
@ -108,12 +120,38 @@ readonly class Option
|
||||
return $this->isNone() ? $this : $f($this->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the option contain the given value?
|
||||
*
|
||||
* @param T $value The value to be checked
|
||||
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`); optional, default is true
|
||||
* @return bool True if the value matches, false if not; `None` always returns false
|
||||
*/
|
||||
public function contains(mixed $value, bool $strict = true): bool
|
||||
{
|
||||
return match (true) {
|
||||
$this->isNone() => false,
|
||||
default => $strict ? $this->value === $value : $this->value == $value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the value of the option match the given predicate function?
|
||||
*
|
||||
* @param callable(T): bool $f The function to determine whether the value matches
|
||||
* @return bool True if the `Some` value matches the function, false otherwise
|
||||
*/
|
||||
public function exists(callable $f): bool
|
||||
{
|
||||
return $this->isSome() ? $f($this->value) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map this optional value to another value
|
||||
*
|
||||
* @template U The type of the mapping function
|
||||
* @param callable(T): U $f The mapping function
|
||||
* @return Option<U> A `Some` instance with the transformed value if `Some`, `None` otherwise
|
||||
* @template TMapped The type of the mapping function
|
||||
* @param callable(T): TMapped $f The mapping function
|
||||
* @return Option<TMapped> A `Some` instance with the transformed value if `Some`, `None` otherwise
|
||||
*/
|
||||
public function map(callable $f): self
|
||||
{
|
||||
@ -140,25 +178,7 @@ readonly class Option
|
||||
*/
|
||||
public function filter(callable $f): self
|
||||
{
|
||||
return match (true) {
|
||||
$this->isNone() => $this,
|
||||
default => $f($this->value) ? $this : self::None(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the option have the given value?
|
||||
*
|
||||
* @param T $value The value to be checked
|
||||
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`)
|
||||
* @return bool True if the value matches, false if not; `None` always returns false
|
||||
*/
|
||||
public function is(mixed $value, bool $strict = true): bool
|
||||
{
|
||||
return match (true) {
|
||||
$this->isNone() => false,
|
||||
default => $strict ? $this->value === $value : $this->value == $value,
|
||||
};
|
||||
return $this->isNone() || $this->exists($f) ? $this : self::None();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,6 +203,16 @@ readonly class Option
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this option into a 0 or 1 item array
|
||||
*
|
||||
* @return T[] An empty array for `None`, a 1-item array for `Some`
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->isSome() ? [$this->value] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this to a PhpOption option
|
||||
*
|
||||
|
@ -103,12 +103,38 @@ readonly class Result
|
||||
return $this->isError() ? $this : $f($this->getOK());
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this result's "OK" value match the given value?
|
||||
*
|
||||
* @param TOK $value The value to be matched
|
||||
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`); optional, default is true
|
||||
* @return bool True if the "OK" value matches the one provided, false otherwise
|
||||
*/
|
||||
public function contains(mixed $value, bool $strict = true): bool
|
||||
{
|
||||
return match (true) {
|
||||
$this->isError() => false,
|
||||
default => $this->okValue->contains($value, $strict),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the "OK" value of this result match the given predicate function?
|
||||
*
|
||||
* @param callable(TOK): bool $f The function to determine whether the value matches
|
||||
* @return bool True if the OK value matches the function, false otherwise
|
||||
*/
|
||||
public function exists(callable $f): bool
|
||||
{
|
||||
return $this->isOK() ? $f($this->okValue->get()) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an `OK` result to another, leaving an `Error` result unmodified
|
||||
*
|
||||
* @template U The type of the mapping function
|
||||
* @param callable(TOK): U $f The mapping function
|
||||
* @return Result<U, TError> A transformed `OK` instance or the original `Error` instance
|
||||
* @template TMapped The type of the mapping function
|
||||
* @param callable(TOK): TMapped $f The mapping function
|
||||
* @return Result<TMapped, TError> A transformed `OK` instance or the original `Error` instance
|
||||
*/
|
||||
public function map(callable $f): self
|
||||
{
|
||||
@ -118,9 +144,9 @@ readonly class Result
|
||||
/**
|
||||
* Map an `Error` result to another, leaving an `OK` result unmodified
|
||||
*
|
||||
* @template U The type of the mapping function
|
||||
* @param callable(TError): U $f The mapping function
|
||||
* @return Result<TOK, U> A transformed `Error` instance or the original `OK` instance
|
||||
* @template TMapped The type of the mapping function
|
||||
* @param callable(TError): TMapped $f The mapping function
|
||||
* @return Result<TOK, TMapped> A transformed `Error` instance or the original `OK` instance
|
||||
*/
|
||||
public function mapError(callable $f): self
|
||||
{
|
||||
@ -139,6 +165,16 @@ readonly class Result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this result into a 0 or 1 item array
|
||||
*
|
||||
* @return TOK[] An empty array for `Error`, a 1-item array for `OK`
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->okValue->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a `Result`'s `OK` value to an `Option`
|
||||
*
|
||||
|
@ -8,6 +8,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Test;
|
||||
|
||||
use BadMethodCallException;
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use InvalidArgumentException;
|
||||
use PhpOption\{None, Some};
|
||||
@ -85,6 +86,20 @@ class OptionTest extends TestCase
|
||||
$this->assertEquals('passed', $value, 'The value should have been obtained from the option');
|
||||
}
|
||||
|
||||
#[TestDox('GetOrThrow succeeds with Some')]
|
||||
public function testGetOrThrowSucceedsWithSome(): void
|
||||
{
|
||||
$value = Option::Some('no throw')->getOrThrow(fn() => new BadMethodCallException('oops'));
|
||||
$this->assertEquals('no throw', $value, 'The "Some" value should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('GetOrThrow succeeds with None')]
|
||||
public function testGetOrThrowSucceedsWithNone(): void
|
||||
{
|
||||
$this->expectException(BadMethodCallException::class);
|
||||
Option::None()->getOrThrow(fn() => new BadMethodCallException('oops'));
|
||||
}
|
||||
|
||||
#[TestDox('Bind succeeds with None')]
|
||||
public function testBindSucceedsWithNone(): void
|
||||
{
|
||||
@ -109,6 +124,54 @@ class OptionTest extends TestCase
|
||||
$this->assertTrue($bound->isNone(), 'The option should have been None');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds with None')]
|
||||
public function testContainsSucceedsWithNone(): void
|
||||
{
|
||||
$this->assertFalse(Option::None()->contains(null), '"None" should always return false');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds with Some when strictly equal')]
|
||||
public function testContainsSucceedsWithSomeWhenStrictlyEqual(): void
|
||||
{
|
||||
$this->assertTrue(Option::Some(3)->contains(3), '"Some" with strict equality should be true');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds with Some when strictly unequal')]
|
||||
public function testContainsSucceedsWithSomeWhenStrictlyUnequal(): void
|
||||
{
|
||||
$this->assertFalse(Option::Some('3')->contains(3), '"Some" with strict equality should be false');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds with Some when loosely equal')]
|
||||
public function testContainsSucceedsWithSomeWhenLooselyEqual(): void
|
||||
{
|
||||
$this->assertTrue(Option::Some('3')->contains(3, strict: false), '"Some" with loose equality should be true');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds with Some when loosely unequal')]
|
||||
public function testContainsSucceedsWithSomeWhenLooselyUnequal(): void
|
||||
{
|
||||
$this->assertFalse(Option::Some('3')->contains(4, strict: false), '"Some" with loose equality should be false');
|
||||
}
|
||||
|
||||
#[TestDox('Exists succeeds with Some when matching')]
|
||||
public function testExistsSucceedsWithSomeWhenMatching(): void
|
||||
{
|
||||
$this->assertTrue(Option::Some(14)->exists(fn($it) => $it < 100), 'Exists should have returned true');
|
||||
}
|
||||
|
||||
#[TestDox('Exists succeeds with Some when not matching')]
|
||||
public function testExistsSucceedsWithSomeWhenNotMatching(): void
|
||||
{
|
||||
$this->assertFalse(Option::Some(14)->exists(fn($it) => $it > 100), 'Exists should have returned false');
|
||||
}
|
||||
|
||||
#[TestDox('Exists succeeds with None')]
|
||||
public function testExistsSucceedsWithNone(): void
|
||||
{
|
||||
$this->assertFalse(Option::None()->exists(fn($it) => true), 'Exists should have returned false');
|
||||
}
|
||||
|
||||
#[TestDox('Map succeeds with None')]
|
||||
public function testMapSucceedsWithNone(): void
|
||||
{
|
||||
@ -188,36 +251,6 @@ class OptionTest extends TestCase
|
||||
$this->assertTrue($filtered->isNone(), 'The filtered option should have been "None"');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with None')]
|
||||
public function testIsSucceedsWithNone(): void
|
||||
{
|
||||
$this->assertFalse(Option::None()->is(null), '"None" should always return false');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with Some when strictly equal')]
|
||||
public function testIsSucceedsWithSomeWhenStrictlyEqual(): void
|
||||
{
|
||||
$this->assertTrue(Option::Some(3)->is(3), '"Some" with strict equality should be true');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with Some when strictly unequal')]
|
||||
public function testIsSucceedsWithSomeWhenStrictlyUnequal(): void
|
||||
{
|
||||
$this->assertFalse(Option::Some('3')->is(3), '"Some" with strict equality should be false');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with Some when loosely equal')]
|
||||
public function testIsSucceedsWithSomeWhenLooselyEqual(): void
|
||||
{
|
||||
$this->assertTrue(Option::Some('3')->is(3, strict: false), '"Some" with loose equality should be true');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with Some when loosely unequal')]
|
||||
public function testIsSucceedsWithSomeWhenLooselyUnequal(): void
|
||||
{
|
||||
$this->assertFalse(Option::Some('3')->is(4, strict: false), '"Some" with loose equality should be false');
|
||||
}
|
||||
|
||||
#[TestDox('Unwrap succeeds with None')]
|
||||
public function testUnwrapSucceedsWithNone(): void
|
||||
{
|
||||
@ -252,6 +285,22 @@ class OptionTest extends TestCase
|
||||
$this->assertSame($original, $tapped, 'The same option should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('ToArray succeeds with Some')]
|
||||
public function testToArraySucceedsWithSome(): void
|
||||
{
|
||||
$arr = Option::Some('15')->toArray();
|
||||
$this->assertNotNull($arr, 'The array should not have been null');
|
||||
$this->assertEquals(['15'], $arr, 'The array was not created correctly');
|
||||
}
|
||||
|
||||
#[TestDox('ToArray succeeds with None')]
|
||||
public function testToArraySucceedsWithNone(): void
|
||||
{
|
||||
$arr = Option::None()->toArray();
|
||||
$this->assertNotNull($arr, 'The array should not have been null');
|
||||
$this->assertEmpty($arr, 'The array should have been empty');
|
||||
}
|
||||
|
||||
#[TestDox('ToPhpOption succeeds for Some')]
|
||||
public function testToPhpOptionSucceedsForSome(): void
|
||||
{
|
||||
|
@ -99,6 +99,55 @@ class ResultTest extends TestCase
|
||||
$this->assertSame($original, $result, 'The same Error result should have been returned');
|
||||
}
|
||||
|
||||
|
||||
#[TestDox('Contains succeeds for Error result')]
|
||||
public function testContainsSucceedsForErrorResult(): void
|
||||
{
|
||||
$this->assertFalse(Result::Error('ouch')->contains('ouch'), '"Error" should always return false');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds for OK result when strictly equal')]
|
||||
public function testContainsSucceedsForOKResultWhenStrictlyEqual(): void
|
||||
{
|
||||
$this->assertTrue(Result::OK(18)->contains(18), '"OK" with strict equality should be true');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds for OK result when strictly unequal')]
|
||||
public function testContainsSucceedsForOKResultWhenStrictlyUnequal(): void
|
||||
{
|
||||
$this->assertFalse(Result::OK(18)->contains('18'), '"OK" with strict equality should be false');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds for OK result when loosely equal')]
|
||||
public function testContainsSucceedsForOKResultWhenLooselyEqual(): void
|
||||
{
|
||||
$this->assertTrue(Result::OK(18)->contains('18', strict: false), '"OK" with loose equality should be true');
|
||||
}
|
||||
|
||||
#[TestDox('Contains succeeds for OK result when loosely unequal')]
|
||||
public function testContainsSucceedsForOKResultWhenLooselyUnequal(): void
|
||||
{
|
||||
$this->assertFalse(Result::OK(18)->contains(17, strict: false), '"OK" with loose equality should be false');
|
||||
}
|
||||
|
||||
#[TestDox('Exists succeeds for OK result when matching')]
|
||||
public function testExistsSucceedsForOKResultWhenMatching(): void
|
||||
{
|
||||
$this->assertTrue(Result::OK(14)->exists(fn($it) => $it < 100), 'Exists should have returned true');
|
||||
}
|
||||
|
||||
#[TestDox('Exists succeeds for OK result when not matching')]
|
||||
public function testExistsSucceedsForOKResultWhenNotMatching(): void
|
||||
{
|
||||
$this->assertFalse(Result::OK(14)->exists(fn($it) => $it > 100), 'Exists should have returned false');
|
||||
}
|
||||
|
||||
#[TestDox('Exists succeeds for Error result')]
|
||||
public function testExistsSucceedsForErrorResult(): void
|
||||
{
|
||||
$this->assertFalse(Result::Error(true)->exists(fn($it) => true), 'Exists should have returned false');
|
||||
}
|
||||
|
||||
#[TestDox('Map succeeds for OK result')]
|
||||
public function testMapSucceedsForOKResult(): void
|
||||
{
|
||||
@ -177,6 +226,22 @@ class ResultTest extends TestCase
|
||||
$this->assertNull($target->called, 'The function should not have been called');
|
||||
}
|
||||
|
||||
#[TestDox('ToArray succeeds for OK result')]
|
||||
public function testToArraySucceedsForOKResult(): void
|
||||
{
|
||||
$arr = Result::OK('yay')->toArray();
|
||||
$this->assertNotNull($arr, 'The array should not have been null');
|
||||
$this->assertEquals(['yay'], $arr, 'The array was not created correctly');
|
||||
}
|
||||
|
||||
#[TestDox('ToArray succeeds for Error result')]
|
||||
public function testToArraySucceedsForErrorResult(): void
|
||||
{
|
||||
$arr = Result::Error('oh no')->toArray();
|
||||
$this->assertNotNull($arr, 'The array should not have been null');
|
||||
$this->assertEmpty($arr, 'The array should have been empty');
|
||||
}
|
||||
|
||||
#[TestDox('ToOption succeeds for OK result')]
|
||||
public function testToOptionSucceedsForOKResult()
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user