https://github.com/klimick/decode
WIP: Decoding untrusted data in typesafe way
https://github.com/klimick/decode
functional functional-programming php psalm psalm-plugin
Last synced: 3 months ago
JSON representation
WIP: Decoding untrusted data in typesafe way
- Host: GitHub
- URL: https://github.com/klimick/decode
- Owner: klimick
- License: mit
- Created: 2021-05-17T00:01:23.000Z (almost 5 years ago)
- Default Branch: main
- Last Pushed: 2022-06-16T12:34:32.000Z (almost 4 years ago)
- Last Synced: 2025-08-02T05:08:19.537Z (8 months ago)
- Topics: functional, functional-programming, php, psalm, psalm-plugin
- Language: PHP
- Homepage:
- Size: 651 KB
- Stars: 8
- Watchers: 4
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
## Decode


[](https://coveralls.io/github/klimick/decode)
This library allow you to take untrusted data and check that it can be represented as type `T`.
- [Usage example](#usage-example)
- [Built in atomics](#builtin-type-atomics)
- [Generic types](#generic-types)
- [Higher order helpers](#higher-order-helpers)
- [Constraints](#constraints)
## Usage example
```php
}
$libraryDefinition = t\shape(
id: t\int(),
name: t\string(),
meta: t\listOf(t\string()),
);
// Untrusted data
$json = '{
"id": 42,
"name": "Decode",
"meta": [
"runtime type system",
"psalm integration",
"with whsv26/functional"
]
}';
// If decode will fail, CastException is thrown.
// $person is array{name: string, age: int, meta: list}
$person = t\tryCast(
value: $json,
to: t\fromJson($libraryDefinition),
);
// Either data type from whsv26/functional
// Left side contains decoding errors
// Right side holds decoded valid
// $person is Either}>>
$personEither = t\decode(
value: $json,
with: t\fromJson($libraryDefinition),
)
// Option data type from whsv26/functional
// $person is Option}>
$personOption = t\cast(
value: $json,
to: t\fromJson($libraryDefinition),
);
```
### Builtin type atomics
##### mixed()
Represents value of any possible type.
##### null()
Represents type for null value.
Suitable for nullable types.
```php
$nullOrInt = union(null(), int())
```
##### int()
Represents integer number.
##### positiveInt()
Represents positive integer number.
##### float()
Represents number with floating point.
##### numeric()
Represents either integer or float numbers.
##### numericString()
Like `numeric()` but represents also string numbers.
#### bool()
Represents boolean value.
#### string()
Represents string value.
#### nonEmptyString()
Represents string that cannot be empty.
#### scalar()
Any scalar value.
#### arrKey()
Represents array key (int | string)
#### datetime()
Represents decoder that can create `DateTimeImmutable` from string.
It uses the constructor of `DateTimeImmutable` by default.
You can specify a format, and then the decoder will be use `DateTimeImmutable::createFromFormat`:
```php
$datetime = datetime(fromFormat: 'Y-m-d H:i:s');
```
It uses UTC timezone by default.
You can pass different time zone during decoder instantiation:
```php
$datetime = datetime(timezone: 'Moscow/Europe');
```
### Generic types
##### union(T1, T2, T3)
Represents type whose value will be of a single type out of multiple types.
```php
// int | string
$intOrString = union(int(), string());
// float | null
$floatOrNull = union(float(), null());
// int | float | string | null
$intOrFloatOrStringOrNull = union($intOrString, $floatOrNull);
```
##### arrayOf(TK, TV)
Represents `array` with keys of type `TK` and values of type `TV`.
```php
// array
$arr = arrayOf(int(), string());
```
##### nonEmptyArrayOf(TK, TV)
Represents `non-empty-array` with keys of type `TK` and values of type `TV`.
```php
// non-empty-array
$nonEmptyArr = nonEmptyArrayOf(int(), string());
```
##### listOf(TV)
Represents `list` with values of type `TV`.
```php
// list
$list = listOf(string());
```
##### nonEmptyListOf(TV)
Represents `non-empty-list` with values of type `TV`.
```php
// non-empty-list
$list = nonEmptyListOf(string());
```
##### shape(prop1: T, prop2: T, propN: T)
Represents `array` with knows keys.
```php
// array{prop1: int, prop2: string, prop3: bool}
$shape = shape(
prop1: int(),
prop2: string(),
prop3: bool(),
);
```
##### partialShape(prop1: T, prop2: T, propN: T)
Like `shape` represents `array` with knows keys, but each key is possibly undefined.
```php
// array{prop1?: int, prop2?: string, prop3?: bool}
$shape = partialShape(
prop1: int(),
prop2: string(),
prop3: bool(),
);
```
##### intersection(T1, T2, T3)
Decoder that allows to combine multiple `shape` or `partialShape` into the one.
```php
// array{prop1: string, prop2: string, prop3?: string, prop4?: string}
$intersection = intersection(
shape(
prop1: string(),
prop2: string(),
),
partialShape(
prop3: string(),
prop4: string(),
),
);
```
##### tuple(T1, T2, T3)
Represents array that indexed from zero with fixed items count.
```php
// array{int, string, bool}
$tuple = tuple(int(), string(), bool());
```
##### object(SomeClass::class)(prop1: T1, prop2: T2, propN: TN)
Allows to create decoder for existed class. For each parameter of the constructor, you must explicitly specify a corresponding decoder.
```php
final class SomeClass
{
public function __construct(
public int $prop1,
public string $prop2,
) {}
/**
* @return DecoderInterface
*/
public static function type(): DecoderInterface
{
return object(self::class)(
prop1: int(),
prop2: string(),
);
}
}
```
##### partialObject(SomeClass::class)(prop1: T1, prop2: T2, propN: T3)
Like `object` decoder, but each parameter of the constructor must be nullable.
##### rec(fn() => T)
Represents recursive type. Only objects can be recursive.
```php
final class SomeClass
{
/**
* @param list $recursive
*/
public function __construct(
public int $prop1,
public string $prop2,
public array $recursive = [],
) { }
/**
* @return DecoderInterface
*/
public static function type(): DecoderInterface
{
$self = rec(fn() => self::type());
return object(self::class)(
prop1: int(),
prop2: string(),
recursive: listOf($self),
);
}
}
```
##### fromJson(T)
Combinator for decoder of type `T` which will be parsed from json representation.
```php
$shapeFromJson = fromJson(
shape(
prop1: string(),
prop2: string(),
)
);
```
### Higher order helpers
##### optional
Allows you to mark property as possibly undefined.
```php
$personD = shape(
name: string(),
additional: listOf(string())->optional(),
);
// inferred type: array{name: string, additional?: list}
$firstShape = tryCast(['name' => 'foo'], $personD);
// No additional field
// ['name' => 'foo']
print_r($firstShape);
// inferred type: array{name: string, additional?: list}
$secondShape = tryCast(['name' => 'foo', 'additional' => ['bar']], $personD);
// ['name' => 'foo', 'additional' => ['bar']]
print_r($secondShape);
```
##### default
Allows you to define a fallback value if an untrusted source does not present one.
```php
$personD = shape(
name: string(),
isEmployed: bool()->default(false),
);
// inferred type: array{name: string, isEmployed: bool}
$firstShape = tryCast(['name' => 'foo'], $personD);
// With default ['isEmployed' => false]
// ['name' => 'foo', 'isEmployed' => false]
print_r($firstShape);
// inferred type: array{name: string, isEmployed: bool}
$secondShape = tryCast(['name' => 'foo', 'isEmployed' => true], $personD);
// ['name' => 'foo', 'isEmployed' => true]
print_r($secondShape);
```
##### constrained
All decoders additionally can be constrained.
```php
$personD = shape(
name: string()->constrained(
minSize(is: 1),
maxSize(is: 255),
),
street: string()->constrained(
minSize(is: 1),
maxSize(is: 255),
),
);
```
[List of builtin constraints](#constraints)
##### from
Helper method `from` is defined for each decoder.
It allows you to specify a path for a result property or rename one.
```php
$personD = shape(
name: string()->from('$.person'),
street: string()->from('$.address.street'),
);
$untrustedData = [
'person' => 'foo',
'address' => [
'street' => 'bar',
],
];
// Inferred type: array{name: string, street: string}
$personShape = tryCast($untrustedData, $personD);
/* Decoded data looks different rather than source: [
'name' => 'foo',
'street' => 'bar',
] */
print_r($personShape);
```
The `$` sign means root of object. You can use just `$` when you want to change decoded structure nesting:
```php
$messengerD = shape(
kind: string()->from('$.messenger_type'),
contact: string()->from('$.messenger_contact'),
);
$personD = shape(
name: string()->from('$.person'),
street: string()->from('$.address.street'),
messenger: $messengerD->from('$'), // means "use the same data for this decoder"
);
$untrustedData = [
'person' => 'foo',
'address' => [
'street' => 'bar',
],
'messenger_type' => 'telegram',
'messenger_contact' => '@Klimick',
];
// inferred type: array{name: string, street: string, messenger: array{kind: string, messenger: string}}
$personShape = tryCast($untrustedData, $personD);
/* Decoded data looks different rather than source: [
'name' => 'foo',
'street' => 'bar',
'messenger' => [
'kind' => 'telegram',
'contact' => '@Klimick',
]
] */
print_r($personShape);
```
### Constraints
Constraints can be attached to decoder with the [constrained](#constrained) higher order helper.
##### equal (all types)
Checks that a numeric value is equal to the given one.
```php
$fooString = string()
->constrained(equal('foo'));
```
##### greater (int, float, numeric)
Checks that a numeric value is greater than the given one.
```php
$greaterThan10 = int()
->constrained(greater(10));
```
##### greaterOrEqual (int, float, numeric)
Checks that a numeric value is greater or equal to the given one.
```php
$greaterOrEqualTo10 = int()
->constrained(greaterOrEqual(10));
```
##### less (int, float, numeric)
Checks that a numeric value is less than the given one.
```php
$lessThan10 = int()
->constrained(less(10));
```
##### lessOrEqual (int, float, numeric)
Checks that a numeric value is less or equal to the given one.
```php
$lessOrEqualTo10 = int()
->constrained(lessOrEqual(10));
```
##### inRange (int, float, numeric)
Checks that a numeric value is in the given range
```php
$from10to20 = int()
->constrained(inRange(10, 20));
```
##### minLength (string, non-empty-string)
Checks that a string value size is not less than given one.
```php
$min10char = string()
->constrained(minLength(10));
```
##### maxLength (string, non-empty-string)
Checks that a string value size is not greater than given one.
```php
$max10char = string()
->constrained(maxLength(10));
```
##### startsWith (string, non-empty-string)
Checks that a string value starts with the given value.
```php
$startsWithFoo = string()
->constrained(startsWith('foo'));
```
##### endsWith (string, non-empty-string)
Checks that a string value ends with the given value.
```php
$endsWithFoo = string()
->constrained(endsWith('foo'));
```
##### uuid (string, non-empty-string)
Checks that a string value is a valid UUID.
```php
$uuidString = string()
->constrained(uuid());
```
##### trimmed (string, non-empty-string)
Checks that a string value has no leading or trailing whitespace.
```php
$noLeadingOrTrailingSpaces = string()
->constrained(trimmed());
```
##### matchesRegex (string, non-empty-string)
Checks that a string value matches the given regular expression.
```php
$stringWithNumbers = string()
->constrained(matchesRegex('/^[0-9]{1,3}$/'));
```
##### forall (array)
Checks that the given constraint holds for all elements of an array value.
```php
$allNumbersGreaterThan10 = forall(greater(than: 10));
$numbersGreaterThan10 = listOf(int())
->constrained($allNumbersGreaterThan10);
```
##### exists (array)
Checks that the given constraint holds for some elements of an array value.
```php
$hasNumbersGreaterThan10 = exists(greater(than: 10));
$withNumberGreaterThan10 = listOf(int())
->constrained($hasNumbersGreaterThan10);
```
##### inCollection (array)
Checks that an array value contains a value equal to the given one.
```php
$listWith10 = listOf(int())
->constrained(inCollection(10));
```
##### maxSize (array)
Checks that an array value size is not greater than the given one.
```php
$max10numbers = listOf(int())
->constrained(maxSize(is: 10));
````
##### minSize (array)
Checks that an array value size is not less than the given one.
```php
$atLeast10numbers = listOf(int())
->constrained(minSize(is: 10));
````
##### allOf (any type)
Conjunction of all constraints.
```php
$from100to200 = allOf(
greaterOrEqual(to: 100),
lessOrEqual(to: 200),
);
$numbersFrom100to200 = listOf(int())
->constrained($from100to200);
```
##### anyOf (any type)
Disjunction of all constraints.
```php
$from100to200 = allOf(
greaterOrEqual(to: 100),
lessOrEqual(to: 200),
);
$from300to400 = allOf(
greaterOrEqual(to: 300),
lessOrEqual(to: 400),
);
$numbersFrom100to200orFrom300to400 = listOf(int())
->constrained(anyOf($from100to200, $from300to400));
```