I've built a thing, don't know if it's useful though... 😅
Tags: dart, flutter, showdev, help
Published: 2022-07-22 09:31:00

Hey there :wave:

Hope you're having a great day! I think I am, as today is the day I announce a project I've been working on for the past weeks. 

I'm happy to introduce you to **Eskema**, a tool to help you validate dynamic data in Dart & Flutter.

The issue is I'm not certain if Eskema is useful or not, so here is where I need your help. Let me know if you think this package could be useful and for what purpose. 

----
Before continuing let me show you a little snippet showcasing what Eskema looks like (_I further explain this later)_:
```dart
final validateMap = eskema({
  'name': isType<String>(),
  'address': nullable(
    eskema({
      'city': isType<String>(),
      'street': isType<String>(),
      'number': all([
        isType<int>(),
        isMin(0),
      ]),
      'additional': nullable(
        eskema({
          'doorbel_number': isType<int>(),
        })
      ),
    })
  )
});
```
----

## Description
**Eskema** is a set of tools to help you validate dynamic data. It allows you to define data "schemes" and "validators" to validate a value, a Map or List for which we don't have the control to correctly type. You can think of it as a really lightweight [json schema](https://json-schema.org/) type thing.

For example, let's say you want to build a library, app or tool where you expect the user to pass in some data but don't want to, or can't use classes or known data structures. This would be the ideal scenario to use **Eskema**. 

## Motivation
The main motivation to build this package was that I did not find a solution to this problem that I liked, and decided to build it myself. I did not have a real use case for this at the moment of building it, but I think there are some scenarios where **Eskema** can be very useful.

Some solutions required to generate code from annotations before being able to validate data, for example using [json_serializable](https://pub.dev/packages/json_serializable). 

If you're already using the json_serializable package, you can make use of it to validate you data. 

I wanted to try and build a tool that didn't require to generate any code, it's all handled at runtime.

### Design
I made sure from the start that the package was flexible, expansible and composable. I also made sure to properly document almost the whole package. I wanted the package to be as well tested as possible, at the moment it has a 100% coverage and is almost fully tested.

The package should offer the basic API and tools to be able to validate any type of data (e.g. Map, List, bool, int, etc...)

I also made sure it would be very simple to create new types of validators as to not limit you!

## How does it work?
The package is really simple, everything in the package are `Validator`s, which are just functions that receive a value and return a `IResult`. 

Parting from this premise, the package adds some helpful `Validators`  to validate single fields, map fields (**eskema**) and list fields (**listEskema**, **listEach**). They all behave in the same way, making composability really straightforwards and powerful.

```dart
final validateMap = eskema({
  'name': isType<String>(),
  'address': nullable(
    eskema({
      'city': isType<String>(),
      'street': isType<String>(),
      'number': all([
        isType<int>(),
        isMin(0),
      ]),
      'additional': nullable(
        eskema({
          'doorbel_number': isType<int>(),
        })
      ),
    })
  )
});
```
> The above example, validates a map against the eskema defined.

## Usage
> NOTE: that if you only want to validate a single value, you probably don't need **Eskema**.

Otherwise let's check how to validate a single value. You can use validators individually:
```dart
final isString = isType<String>();
const result1 = isString('valid string');
const result2 = isString(123);

result1.isValid;  // true
result2.isValid;  // false
result2.expected; // String
```


Or you can combine validators: 
```dart
all([isType<String>(), isDate()]);     // all validators must be valid
or(isType<String>(), isType<int>());   // either validator must be valid
and(isType<String>(), isType<int>());  // both validator must be valid


// This validator checks that, the value is a list of strings, with length 2, and contains item "test"
all([
  isOfLength(2),                    // checks that the list is of length 2
  listEach(isTypeOrNull<String>()), // checks that each item is either string or null
  listContains('test'),             // list must contain value "test"
]);

// This validator checks a map against a eskema. Map must contain property 'books', 
// which is a list of maps that matches a sub-eskema.
final matchesEskema = eskema({
  'books': listEach(
    eskema({
      'name': isType<String>(),
    }),
  ),
});
matchesEskema({'books': [{'name': 'book name'}]});
```

## Validators

### isType<T>
This validator checks that a value is of a certain type
```dart
isType<String>();
isType<int>();
isType<double>();
isType<List>();
isType<Map>();
```

### isTypeOrNull<T>
This validator checks that a value is of a certain type or is null
```dart
isTypeOrNull<String>();
isTypeOrNull<int>();
```

### nullable
This validator allows to make validators allow null values
```dart
nullable(eskema({...}));
```
* The validator above, allows a map or null

### eskema
The most common use case will probably be validating JSON or dynamic maps. For this, you can use the `eskema` validator.

In this example we validate a Map with optional fields and with nested fields.
```dart
final validateMap = eskema({
  'name': isType<String>(),
  'address': nullable(
    eskema({
      'city': isType<String>(),
      'street': isType<String>(),
      'number': all([
        isType<int>(),
        isMin(0),
      ]),
      'additional': nullable(
        eskema({
          'doorbel_number': Field([isType<int>()])
        })
      ),
    })
  )
});

final invalidResult = validateMap({});
invalidResult.isValid;    // false
invalidResult.isNotValid; // true
invalidResult.expected;   // name -> String
invalidResult.message;    // Expected name -> String

final validResult = validateMap({ 'name': 'bobby' });
validResult.isValid;    // true
validResult.isNotValid; // false
validResult.expected;   // Valid
```

### listEach
The other common use case is validating dynamic Lists. For this, you can use the `listEach` class.

This example validates that the provided value is a List of length 2, and each item must be of type int:
```dart
final isValidList = all([
    listOfLength(2),
    listEach(isType<int>()),
]);

isValidList(null).isValid;      // true
isValidList([]).isValid;        // true
isValidList([1, 2]).isValid;    // true
isValidList([1, "2"]).isValid;  // false
isValidList([1, "2"]).expected; // [1] -> int
```



### Additional Validators
For a complete list of validators, check the [docs](https://pub.dev/documentation/eskema/latest/)

### Custom Validators
**Eskema** offers a set of common Validators located in `lib/src/validators.dart`. You are not limited to only using these validators, custom ones can be created very easily. 

Let's see how to create a validator to check if a string matches a pattern:

```dart
Validator validateRegexp(RegExp regexp) {
  return (value) {
    return Result(
      isValid: regexp.hasMatch(value),  
      expected: 'match pattern $regexp', // the message explaining what this validator expected
    );
  };
}
```


## Summary
Just to finish up, I want to thank you for taking the time to read though all of that. If you liked the project please feel free to share and/or star it on GitHub, so it can reach more people who might find it useful.

Remember to let me know if you think this package is useful to you and why and where would you use it!

## Help is encouraged!
If you feel like helping out, have any ideas or just want to support this project please feel free to do so!!

## Additional information

* Repository [here](https://github.com/nombrekeff/eskema)
* For more information check the [docs](https://pub.dev/documentation/eskema/latest/)
 out. 
* If you find a bug please file an [issue](https://github.com/nombrekeff/eskema/issues/new) or send a PR my way.
* Contributions are welcomed, feel free to send in fixes, new features, custom validators, etc...