Skip to content

Commit

Permalink
Introduce readonly lenses
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Dec 19, 2024
1 parent e5d015b commit 46bd401
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
"src/Lens/optional.php",
"src/Lens/compose.php",
"src/Lens/index.php",
"src/Lens/property.php",
"src/Lens/properties.php",
"src/Lens/property.php",
"src/Lens/read_only.php",
"src/Iso/compose.php",
"src/Iso/object_data.php",
"src/Reflect/class_attributes.php",
Expand Down
34 changes: 34 additions & 0 deletions docs/lens.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,37 @@ $propertyLens->set(new Item(), ['value': 'world']);
// > Item { value: "world" }
```

#### read_only

This function will create a lens that can only be used to get the value of the provided value.

* **Get** will try to get() from the decorated lens
* **Set** will throw a `ReadonlyException` when trying to set a value.

```php
use function VeeWee\Reflecta\Lens\property;
use function VeeWee\Reflecta\Lens\read_only;

$readonlyValueLens = read_only(property('value'));

$readonlyValueLens->get(new class {
public string $value = 'hello';
});
// > "hello"


$optionalValueLens->set(new class {
public string $value = 'hello';
}, 'world');

// > Throws ReadonlyException
```

The main Lens class has a shortcut function as well to create a readonly lens from a getter:

```php
use VeeWee\Reflecta\Lens\Lens;

$getter = fn (mixed $item): mixed => $item;
$lens = Lens::readonly($getter);
```
11 changes: 11 additions & 0 deletions src/Exception/ReadonlyException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Exception;

final class ReadonlyException extends RuntimeException
{
public static function couldNotWrite(): self
{
return new self('Could not write to the provided lens: it is readonly.');
}
}
13 changes: 13 additions & 0 deletions src/Lens/Lens.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace VeeWee\Reflecta\Lens;

use Psl\Result\ResultInterface;
use VeeWee\Reflecta\Exception\ReadonlyException;
use function Psl\Result\wrap;

/**
Expand Down Expand Up @@ -36,6 +37,18 @@ public function __construct(callable $get, callable $set)
$this->set = $set;
}

/**
* @pure
* @template RS
* @template RA
* @param callable(RS): RA $get
* @return Lens<RS, RA>
*/
public static function readonly(callable $get): self
{
return new self($get, static fn ($s, $a) => throw ReadonlyException::couldNotWrite());
}

/**
* @pure
* @template I
Expand Down
24 changes: 24 additions & 0 deletions src/Lens/read_only.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Lens;

/**
* @template S
* @template A
*
* @param LensInterface<S, A> $that
*
* @return Lens<S, A>
*
* @psalm-pure
*/
function read_only(LensInterface $that): Lens
{
return Lens::readonly(
/**
* @param S $subject
* @return A
*/
static fn ($subject) => $that->get($subject)
);
}
23 changes: 23 additions & 0 deletions tests/unit/Exception/ReadonlyExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\UnitTests\Exception;

use PHPUnit\Framework\TestCase;
use VeeWee\Reflecta\Exception\ReadonlyException;
use VeeWee\Reflecta\Exception\RuntimeException;

final class ReadonlyExceptionTest extends TestCase
{

public function test_it_can_throw_readonly_error(): void
{
$exception = ReadonlyException::couldNotWrite();

$this->expectExceptionObject($exception);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Could not write to the provided lens: it is readonly.');

throw $exception;
}
}
17 changes: 17 additions & 0 deletions tests/unit/Lens/LensTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

use PHPUnit\Framework\TestCase;
use RuntimeException;
use VeeWee\Reflecta\Exception\ReadonlyException;
use VeeWee\Reflecta\Lens\Lens;
use function array_key_exists;
use function Psl\Fun\identity;
use function VeeWee\Reflecta\Lens\index;

final class LensTest extends TestCase
Expand Down Expand Up @@ -106,4 +108,19 @@ public function test_it_can_compose_lenses(): void
static::assertSame('hello', $composed->get($data));
static::assertSame(['greet' => ['message' => 'goodbye']], $composed->set($data, 'goodbye'));
}

public function test_it_can_read_from_readonly_lens(): void
{
$lens = Lens::readonly(identity());

static::assertSame('result', $lens->get('result'));
}

public function test_it_can_not_write_to_readonly_lens(): void
{
$lens = Lens::readonly(identity());

$this->expectExceptionObject(ReadonlyException::couldNotWrite());
$lens->set('result', 'impossible');
}
}
24 changes: 24 additions & 0 deletions tests/unit/Lens/ReadonlyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\UnitTests\Lens;

use PHPUnit\Framework\TestCase;
use VeeWee\Reflecta\Exception\ReadonlyException;
use function VeeWee\Reflecta\Lens\property;
use function VeeWee\Reflecta\Lens\read_only;

final class ReadonlyTest extends TestCase
{

public function test_it_can_be_readonly(): void
{
$lens = read_only(property('foo'));
$data = (object) ['foo' => 'bar'];

static::assertSame('bar', $lens->get($data));

$this->expectExceptionObject(ReadonlyException::couldNotWrite());
$lens->set('hello', $data);
}
}

0 comments on commit 46bd401

Please sign in to comment.