psl icon indicating copy to clipboard operation
psl copied to clipboard

Improvement converted json type

Open filippeb opened this issue 1 year ago • 5 comments

Is your feature request related to a problem? Not a problem, a proposal for a convenience addition to \Psl\Json\typed conversion.

Describe the solution you'd like A converted type function for json data (see below snippet) usable on convert shapes.

Describe alternatives you've considered Can be achieved with current Psl but is a bit more verbose

Snippet

/**
 * @template D
 * @template O
 *
 * @param TypeInterface<D>     $decoded   decoded json type
 * @param TypeInterface<O>     $into
 * @param null|(Closure(D): O) $converter argument is the decoded json value of type $from
 *
 * @return TypeInterface<O>
 */
function convertedJson(TypeInterface $decoded, TypeInterface $into, ?\Closure $converter = null): TypeInterface
{
    return new Internal\ConvertedType(
        string(),
        $into,
        null === $converter
            ? static fn(string $json): mixed => $into->assert(typed($json, $decoded)) // if the decoded json type is the same as the 'into' type
            : static fn(string $json): mixed => $converter(typed($json, $decoded))
    );
}

filippeb avatar Apr 18 '24 14:04 filippeb

Some examples:

In short: Converting a list of string id's to a collection of id's

<?php
final readonly class Id
{
    public function __construct(public string $id)
    {
    }

    public static function type(): TypeInterface
    {
        return converted(
            string(),
            instance_of(__CLASS__),
            static fn(string $id) => new self($id)
        );
    }
}

final readonly class IdCollection
{
    public function __construct(
        public array $ids
    ) {
    }

    public static function type(): TypeInterface
    {
        return converted(
            vec(
                Id::type()
            ),
            instance_of(__CLASS__),
            static fn(array $ids) => new self($ids)
        );
    }
}

/** shape containing the full convert type */
shape([
    'key' => converted(
        string(),
        instance_of(IdCollection::class),
        static fn(string $json) => typed(
            $json,
            converted(
                vec(
                    string()
                ),
                instance_of(IdCollection::class),
                static fn(array $ids): IdCollection => new IdCollection(
                    map(
                        $ids,
                        static fn(string $id): Id => new Id($id)
                    )
                )
            )
        )
    ),
])->coerce(
    ['key' => '["id-one","id-two"]']
);

/** same with abstracted types (already possible with current Psl) */
shape([
    'key' => converted(
        string(),
        instance_of(IdCollection::class),
        static fn(string $json): IdCollection => typed(
            $json,
            IdCollection::type()
        )
    ),
])->coerce(
    ['key' => '["id-one","id-two"]']
);

/** convertedJson with converter Closure */
shape([
    'key' => convertedJson(
        IdCollection::type(),
        instance_of(IdCollection::class),
        static fn(IdCollection $collection): IdCollection => $collection
    ),
])->coerce(
    ['key' => '["id-one","id-two"]']
);

/** convertedJson without converter */
$shape = shape([
    'key' => convertedJson(
        IdCollection::type(),
        instance_of(IdCollection::class),
    ),
])->coerce(
    ['key' => '["id-one","id-two"]']
);

filippeb avatar Apr 18 '24 14:04 filippeb

Penny for your thoughts on this

filippeb avatar Apr 18 '24 14:04 filippeb

Something like this could work:

namespace Psl\Type;

use Psl\Json;

/**
 * @psalm-pure
 *
 * @template T
 *
 * @param TypeInterface<T> $value_type
 *
 * @return TypeInterface<T>
 */
function json(TypeInterface $value_type): TypeInterface
{
    return converted(
        non_empty_string(),
        $value_type,
        static fn (string $json): mixed => Json\typed($json, $value_type)
    );
}

Where you could do:

Type\Json(IdCollection::type())

Having an additional callback would be a bridge too far for me. You could always do that in your IdCollection::type() which already contains a converted type.

@azjezz WDYT? In the future we could introduce similar converted shortcuts like these, e.g. for Type\date_time($format, $timezone) without having to specifying a dedicated internal type for them?

veewee avatar Apr 19 '24 08:04 veewee

@veewee that would indeed do the job.

filippeb avatar Apr 22 '24 06:04 filippeb

@azjezz WDYT?

I like the idea, but maybe a different name than Type\json? something like Type\from_json() that indicate that the value would come from json?

azjezz avatar Apr 22 '24 06:04 azjezz