Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

express-validator like validation #74

Closed
tomassasovsky opened this issue Nov 26, 2021 · 5 comments
Closed

express-validator like validation #74

tomassasovsky opened this issue Nov 26, 2021 · 5 comments

Comments

@tomassasovsky
Copy link

Since this is a package that resembles expressjs very much, I think some implementation of the express-validator would be great. Is there any way to implement something like this?

@rknell
Copy link
Owner

rknell commented Nov 27, 2021

Thanks @tomassasovsky

One of the key things with this project is to try and keep it as nimble as possible while still doing most of the stuff you want, so I would be happy to have some validator functions, but probably in a separate package, however make it easy to throw "Alfred exceptions" which are easily configurable.

I was recently building an accounting system in Alfred that needed to be absolutely bulletproof, and my method for validation (and extraction of the request variables) used this sort of method which worked quite well. Didn't get to the point of checking if its "is email" though, but it would get there at some point.

    final body = await req.bodyAsJsonMap;
    final amount = requiredParamProperty<double>(body, 'amount');
    final email = requiredParamProperty<String>(body, 'email');

this was the implementation (I know it looks like a mess) but it worked really well:

T requiredParamProperty<T>(dynamic object, String name,
        {bool isPositive = false, bool isNotEmpty = true}) =>
    requiredParam(object[name], name,
        isPositive: isPositive, isNotEmpty: isNotEmpty);

T requiredParam<T>(dynamic input, String name,
    {bool isPositive = false, bool isNotEmpty = true}) {
  if (input == null) {
    throw InvalidParametersException(name);
  }
  if (input is String && input.isEmpty) {
    throw InvalidParametersException(name);
  }

  if (T == num || T == int || T == double) {
    if (isPositive == true &&
        (input is String ? num.parse(input) : input) <= 0) {
      throw InvalidParametersException(name);
    }
    if (T == int && input is String) {
      return num.parse(input).toInt() as T;
    }
    if (T == double && input is String) {
      return num.parse(input).toDouble() as T;
    }

    if (T == num && input is String) {
      return num.parse(input) as T;
    }
  }
  if (input is T == false) {
    throw InvalidParametersException(name);
  } else {
    return input as T;
  }
}

You could probably take this a step further and have something like

[
     RequiredBodyProperty<Email>('email')
];

As a validation middleware that checks if the body has a key named 'email', and its actually an email address. But you still need to pull it out again in the next step. You could just create a validator type class that is populated from a body request and performs the validation along the way though. Its a bit manual but you have full control.

HOWEVER - this traditional way of building servers has become pretty tedious to me. My next project with alfred I is probably going to be an "Alfred generator" similar to some other languages out there that describe API servers - something like swagger, but making the process as seamless as possible with an importable client, strong data types and using dart's type system to easily alert you about the implications of a change. I think thats going to be the best place to focus the energy on locking down types, and something that can have a bit more meat in it (plus it will probably have a package for easy import too!). Dart needed an easy to use but not overly complicated server after Angel and Aqueduct pulled up stumps and if I included too much it would get stuck in the same trap, so the idea is that even if I were hit by a bus or had a mental breakdown, if you built your system on Alfred you should be able to pick it up and continue with it in about 24 hours.

I'm going to close this for now as I will probably pick up further work in another project, but feel free to reply / I can open it if need be.

@rknell rknell closed this as completed Nov 27, 2021
@d-markey
Copy link
Collaborator

d-markey commented Dec 1, 2021

Hello Ryan,

very interested in your "Alfred generator".

FYI I was working on something maybe similar or maybe just remotely connected, I don't reallly know what's on your mind in terms of "Alfred generator". Anyway, my project aims at automatically providing a description of APIs implemented with Alfred and derive OpenApi specs directly from code. I've posted my code so far on https://github.com/d-markey/alfredoc (I'm very bad at choosing names) and implemented a sample in https://github.com/d-markey/alfredoc_sample.

My first attempt was totally manual (and tedious as you'd spend so much time keep things in sync) so I've added annotations + code generation to automatically describe payloads.

Plan is to do the same for APIs, it would be great to have annotations and generate the code for all the routes and the bindings. Too bad Dart doen't have introspection built-in... Dart mirrors could be a way to go too, but it's unstable at the moment. Right now my implementation for "OpenApi" routes is manual, yet somewhat integrable with Alfred.

I'd very much like to hear your feedback if/when you have time to look at it :)

( apologies for the wrong name! )

@rknell
Copy link
Owner

rknell commented Dec 4, 2021

Hey @d-markey ,

That looks awesome and is way more advanced than anything I have done I haven't dug into it yet but totally the direction I was planning on going. When I get a chance I'll give it a try (and maybe help out if thats ok?) and once its ready to roll lets promote it on the alfred readme hey?

@d-markey
Copy link
Collaborator

d-markey commented Dec 6, 2021 via email

@tomassasovsky
Copy link
Author

tomassasovsky commented Apr 20, 2022

So I've been working a bit on this... Especially on returning multiple errors in one request.

I've inspired the error saving on the StorePlugin.

This is the code for the plugin:

// error_extension.dart

import 'package:alfred/alfred.dart';

/// Data structure to keep all request-related data
final errorPluginData = <HttpRequest, ErrorStore>{};

/// Integrates [ErrorStore] mechanism on [HttpRequest]
extension ErrorPlugin on HttpRequest {
  /// Returns the [ErrorStore] dedicated to this request.
  ErrorStore get errorStore {
    errorPluginData[this] ??= ErrorStore();
    return errorPluginData[this]!;
  }

  /// If the errors list is not empty, it throws an [AlfredException] containing the errors.
  void validate() {
    final errors = errorStore.errors;
    if (errors.isNotEmpty) {
      throw AlfredException(400, {
        'errors': errors.map((e) => e.toJson()).toList(),
      });
    }
  }
}

/// Stores the errors for an [HttpRequest]
class ErrorStore {
  ErrorStore();

  final _errors = <ValidationError>[];

  void add(ValidationError error) => _errors.add(error);

  List<ValidationError> get errors => _errors;
}

void errorPluginOnDoneHandler(HttpRequest req, HttpResponse res) {
  errorPluginData.remove(req);
}

class ValidationError {
  ValidationError({
    this.location,
    this.msg,
    this.param,
  });

  Map<String, String> toJson() {
    return <String, String>{
      if (location != null && (location?.isNotEmpty ?? false)) 'location': location!,
      if (msg != null && (msg?.isNotEmpty ?? false)) 'msg': msg!,
      if (param != null && (param?.isNotEmpty ?? false)) 'param': param!,
    };
  }

  final String? location;
  final String? msg;
  final String? param;
}

We have the validator class here:

// input_variable_validator.dart

import 'dart:async';
import 'dart:mirrors';

import 'package:alfred/alfred.dart';
import './error_extension.dart';

enum Source {
  body,
  query,
  headers,
}

enum ErrorType {
  parameterNotFound,
  parameterTypeMismatch,
}

class InputVariableValidator<T> {
  InputVariableValidator(
    this.req,
    this.name, {
    this.source = Source.body,
  });

  final String name;
  final Source source;
  final HttpRequest req;

  FutureOr<T> required() async {
    final dynamic value = await _parseParameter();

    if (value == null || (value is String && value.isEmpty)) {
      _addError(value, ErrorType.parameterNotFound);
      return _createInstanceOf();
    } else if ((T == num || T == int || T == double) && value is String) {
      final asNum = num.tryParse(value);
      if (asNum == null) {
        _addError(value, ErrorType.parameterTypeMismatch);
        return _createInstanceOf();
      }
      return asNum as T;
    } else if (value is! T) {
      _addError(value, ErrorType.parameterTypeMismatch);
      return _createInstanceOf();
    }
    return value;
  }

  Future<T?> nullable() async {
    final dynamic value = await _parseParameter();

    if (value == null || (value is String && value.isEmpty)) {
      return null;
    } else if ((T == num || T == int || T == double) && value is String) {
      final asNum = num.tryParse(value);
      if (asNum == null) {
        _addError(value, ErrorType.parameterTypeMismatch);
        return null;
      }
      return asNum as T;
    } else if (value is! T) {
      _addError(value, ErrorType.parameterTypeMismatch);
      return null;
    }
    return value;
  }

  FutureOr<dynamic> _parseParameter() async {
    dynamic value;
    switch (source) {
      case Source.body:
        final body = await req.bodyAsJsonMap;
        if (body.containsKey(name)) {
          value = body[name];
        }
        break;
      case Source.query:
        if (req.uri.queryParameters.containsKey(name)) {
          value = req.uri.queryParameters[name];
        }
        break;
      case Source.headers:
        if (req.headers.value(name) != null) {
          value = req.headers.value(name);
        }
        break;
    }
    return value;
  }

  void _addError(
    dynamic value,
    ErrorType errorType,
  ) {
    final message = errorType == ErrorType.parameterNotFound ? 'Parameter not found' : 'Parameter is not of type $T';
    req.errorStore.add(
      ValidationError(
        location: source.name,
        msg: message,
        param: name,
      ),
    );
  }

  T _createInstanceOf() {
    switch (T) {
      case String:
        return '' as T;
      case num:
        return 0 as T;
      case bool:
        return false as T;
      case DateTime:
        return DateTime.now() as T;
      case List:
        return <dynamic>[] as T;
      case Map:
        return <dynamic, dynamic>{} as T;
      default:
        final mirror = reflectClass(T);
        return mirror.newInstance(Symbol.empty, <dynamic>[]).reflectee as T;
    }
  }
}

And an example of its implementation:

import 'package:alfred/alfred.dart';

import './input_variable_validator.dart';
import './error_extension.dart';

Future<void> main() async {
  final app = Alfred();

  app.post('/login', (req, res) {
    final username = await InputVariableValidator<String>(req, 'username').required();
    final password = await InputVariableValidator<String>(req, 'password').required();
    req.validate();
  });

  // register this function to delete the errors of a specific request after returning a response
  app..registerOnDoneListener(errorPluginOnDoneHandler);

  await app.listen(8080);
}

This is an example of how the app reacts to a request with this body:

{
    "username": 123
    // notice the lack of the password argument
}
{
    "errors": [
        {
            "location": "body",
            "msg": "Parameter is not of type String",
            "param": "username"
        },
        {
            "location": "body",
            "msg": "Parameter not found",
            "param": "password"
        }
    ]
}

This is a highly customizable implementation. The error messages can be changed around, new use cases can be easily implemented.

I hope this is useful :))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants