I've built a thing, don't know if it's useful though... 😅

Keff - Jul 22 '22 - - Dev Community

Hey there 👋

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):

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>(),
        })
      ),
    })
  )
});
Enter fullscreen mode Exit fullscreen mode

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 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.

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 Validators, 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.

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>(),
        })
      ),
    })
  )
});
Enter fullscreen mode Exit fullscreen mode

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:

final isString = isType<String>();
const result1 = isString('valid string');
const result2 = isString(123);

result1.isValid;  // true
result2.isValid;  // false
result2.expected; // String
Enter fullscreen mode Exit fullscreen mode

Or you can combine validators:

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'}]});
Enter fullscreen mode Exit fullscreen mode

Validators

isType

This validator checks that a value is of a certain type

isType<String>();
isType<int>();
isType<double>();
isType<List>();
isType<Map>();
Enter fullscreen mode Exit fullscreen mode

isTypeOrNull

This validator checks that a value is of a certain type or is null

isTypeOrNull<String>();
isTypeOrNull<int>();
Enter fullscreen mode Exit fullscreen mode

nullable

This validator allows to make validators allow null values

nullable(eskema({...}));
Enter fullscreen mode Exit fullscreen mode
  • 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.

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

Additional Validators

For a complete list of validators, check the docs

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:

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

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
  • For more information check the docs out.
  • If you find a bug please file an issue or send a PR my way.
  • Contributions are welcomed, feel free to send in fixes, new features, custom validators, etc...
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .