On 2013-11-25 08:24, Meta wrote:
- The function validated would probably be better named validate, since
it actually performs validation and returns a validated type. The
struct's name is fine.

Yeah, I was somewhat torn there, but I think you're right. Fixed.


- I think it'd be better to change "static if (is(typeof(fn(value)) ==
bool))" to "static if (is(typeof(fn(value)) : bool))", which rather than
checking that the return type is exactly bool, it only checks that it's
implicitly convertible to bool, AKA "truthy".

Even better - test if 'if (fn(value)) {}' compiles. Fixed.


- It might be a good idea to have a version(AlwaysValidate) block in
assumeValidated for people who don't care about code speed and want
maximum safety, that would always run the validation functions. Also, it
might be a good idea to mark assumeValidated @system, because it
blatantly breaks the underlying assumptions being made in the first
place. Code that wants to be rock-solid @safe will be restricted to
using only validate. Or maybe that's going too far.

@safe is only for memory safety, which this is not. I agree it would be nice to mark assumeValidated as 'warning, may not do what it claims', but @safe is not really the correct indicator of that.


- Validated doesn't work very well with reference types. The following
fails:

class CouldBeNull
{
}

bool notNull(T)(T t)
if (is(T == class))
{
     return t !is null;
}

//Error: cannot implicitly convert expression (this._value) of type
inout(CouldBeNull) to f505.CouldBeNull
void takesNonNull(Validated!(CouldBeNull, notNull) validatedT)
{
}

Yeah, found that. It's a bug in value(), which should return inout(T), not T. Fixed.


- On the subject of reference types, I don't think Validated handles
them quite correctly. This is a problem I ran into, and it's not an easy
one. Assume for a second that there's a class FourtyTwo that *does* work
with Validated:

     class FortyTwo
     {
         int i = 42;
     }

     bool containsFortyTwo(FortyTwo ft)
     {
         return ft.i == 42;
     }

     void mutateFortyTwo(Validated!(FortyTwo, containsFortyTwo) fortyTwo)
     {
         fortyTwo.i = 43;
     }

     auto a = validated!containsFortyTwo(new FortyTwo());
     auto b = a;
     //Passes
     assert(a.i == 42);
     assert(b.i == 42);
     mutateFortyTwo(a);
     //Fails
     assert(a.i == 43);
     assert(b.i == 43);

This is an extremely contrived example, but it illustrates the problem
of using reference types with Validated. It gets even hairier if i
itself were a reference type, like a slice:

     void mutateCopiedValue(Validated!(FortyTwo, containsFortyTwo)
fortyTwo)
     {
         //We're not out of the woods yet
         int[] arr = fortyTwo.i;
         arr[0] += 1;
     }

         //Continuing from previous example,
         //except i is now an array
     mutateCopiedValue(b);
     assert(a.i[0] == 44);
     assert(b.i[0] == 44);

Obviously in this case you could just .dup i, but what if i were a class
itself? It'd be extremely easy to accidentally invalidate every
Validated!(FortyTwo, ...) in the program in a single swipe. It gets even
worse if i were some class reference to which other, non-validated
references existed. Changing those naked references would change i, and
possibly invalidate it.

This is a known shortcoming for which I see no good workaround. It would be possible to use std.traits.hasAliasing to see which types can be safely .dup'ed and only allow those types, but this is not a solution I like.

I guess it could print a warning when used with unsafe types. If I were to do that, I would still want some way to turn that message off. Eh. Maybe there is no good solution.


What else is new?
- Better error messages for invalid constraints (testing if an int is
  null, a string is divisible by 3 or an array has a database
  connection, e.g.)
- Fixed a bug in opCast (I love that word - in Norwegian it [oppkast]
  means puke. ...anyways...) when converting to an incompatible wrapped
  value.

--
  Simen

module biotronic.utils;

import std.typetuple : TypeTuple, NoDuplicates, staticIndexOf;
import std.traits : Unqual, ParameterTypeTuple;

void staticEnforce(bool criteria, string msg)() {
    static if (!criteria) {
        pragma(msg, msg);
        static assert(false);
    }
}

void staticEnforce(bool criteria, string msg, string file, int line)() {
    staticEnforce!(criteria, file ~ "(" ~ line.stringof ~ "): Error: " ~ msg);
}

auto sum( R )( R range ) if ( isInputRange!R ) {
    ElementType!R tmp = 0;
    return reduce!( (a,b)=>a+b )( tmp, range );
}

template arrayToTuple( alias name ) {
    static if ( name.length ) {
        alias arrayToTuple = TypeTuple!( name[0], arrayToTuple!( name[1..$] ) );
    } else {
        alias arrayToTuple = TypeTuple!( );
    }
}

template Repeat( size_t n, T... ) {
    static if ( n ) {
        alias Repeat = TypeTuple!( T, Repeat!( n-1, T ) );
    } else {
        alias Repeat = TypeTuple!();
    }
}

template hasFloatBehavior( T ) {
    static if ( __traits( compiles, { T t; t = 1; return (t/2)*2 == t; } ) ) {
        enum hasFloatBehavior = { T t; t = 1; return (t/2)*2 == t; }();
    } else {
        enum hasFloatBehavior = false;
    }
} unittest {
    assert( hasFloatBehavior!float );
    assert( hasFloatBehavior!double );
    assert( hasFloatBehavior!real );
    assert( !hasFloatBehavior!int );
    assert( !hasFloatBehavior!char );
    assert( !hasFloatBehavior!string );
}

template hasNumericBehavior( T ) {
    template hasNumericBehaviorImpl( U... ) {
        static if ( U.length ) {
            enum hasNumericBehaviorImpl = is( Unqual!T == U[0] ) || 
hasNumericBehaviorImpl!( U[1..$] );
        } else {
            enum hasNumericBehaviorImpl = false;
        }
    }
    
    enum hasNumericBehavior = hasNumericBehaviorImpl!( byte, short, int, long, 
ubyte, ushort, uint, ulong, float, double, real );
} unittest {
    foreach ( Type; TypeTuple!( byte, short, int, long, ubyte, ushort, uint, 
ulong, float, double, real ) ) {
        assert( hasNumericBehavior!Type );
    }
    foreach ( Type; TypeTuple!( string, char, dchar, int[], void, void*) ) {
        assert( !hasNumericBehavior!Type );
    }
}

template StaticFilter(alias pred, T...) {
    static if (T.length == 0) {
        alias StaticFilter = TypeTuple!();
    } else static if (T.length == 1) {
        static if (pred!(T[0])) {
            alias StaticFilter = T;
        } else {
            alias StaticFilter = TypeTuple!();
        }
    } else {
        alias StaticFilter = TypeTuple!(
            StaticFilter!(pred, T[0..$/2]),
            StaticFilter!(pred, T[$/2..$]));
    }
}

struct CMP(T...){}

template sortPred(T...) if (T.length == 2) {
    static if ( TypeTuple!(T[0]).stringof < TypeTuple!(T[1]).stringof ) {
        enum sortPred = -1;
    } else static if ( TypeTuple!(T[0]).stringof > TypeTuple!(T[1]).stringof ) {
        enum sortPred = 1;
    } else {
        enum sortPred = 0;
    }
} unittest {
    assert( sortPred!(int, string) == -sortPred!( string, int ) );
}

template StaticSort(alias pred, T...) {
    static if (T.length == 0) {
        alias StaticSort = TypeTuple!();
    } else static if (T.length == 1) {
        alias StaticSort = T;
    } else {
        template lessPred(U...) {
            enum lessPred = pred!(T[0], U[0]) == 1;
        }
        template equalPred(U...) {
            enum equalPred = pred!(T[0], U[0]) == 0;
        }
        template morePred(U...) {
            enum morePred = pred!(T[0], U[0]) == -1;
        }
        
        
        alias eq = StaticFilter!(equalPred, T);
        alias less = StaticFilter!(lessPred, T);
        alias more = StaticFilter!(morePred, T);
        
        alias StaticSort = TypeTuple!(
            StaticSort!(pred, less),
            eq,
            StaticSort!(pred, more));
    }
} unittest {
    assert(is(StaticSort!(sortPred, int, string) == StaticSort!(sortPred, 
string, int)));
    assert(is(StaticSort!(sortPred, int, string) == StaticSort!(sortPred, 
string, int)));
    
    assert(is(CMP!(StaticSort!(sortPred, int, "waffles", string)) == 
CMP!(StaticSort!(sortPred, "waffles", string, int))));
}

template hasNoDuplicates( T... ) {
    enum hasNoDuplicates = is( CMP!T == CMP!(NoDuplicates!T) );
}

template isSorted( T... ) {
    enum isSorted = is( CMP!T == CMP!(StaticSort!( sortPred, T ) ) );
} unittest {
    assert( isSorted!() );
    assert( isSorted!int );
    assert( isSorted!(int, int) );
    assert( isSorted!(int, string) );
    assert( !isSorted!(string, int) );
}

template TypeEnum(T...) {
    template TypeEnumName(int n) {
        static if (n < T.length) {
            enum TypeEnumName = "_" ~ n.stringof ~ "," ~ TypeEnumName!(n+1);
        } else {
            enum TypeEnumName = "";
        }
    }
    mixin("enum TypeEnum {"~TypeEnumName!0~"}");
}
    
template ParameterTypeTupleOrVoid(T...) if (T.length == 1) {
    static if (is(ParameterTypeTuple!T)) {
        alias ParameterTypeTupleOrVoid = CMP!(ParameterTypeTuple!T);
    } else {
        alias ParameterTypeTupleOrVoid = CMP!void;
    }
}

template isType(T...) if (T.length == 1) {
    enum isType = is(T[0]);
}

template TypeSet(T...) {
    template superSetOf(U...) {
        static if (U.length == 0) {
            enum superSetOf = true;
        } else static if (U.length == 1) {
            enum superSetOf = staticIndexOf!(U, T) != -1;
        } else {
            enum superSetOf = superSetOf!(U[0..$/2]) && superSetOf!(U[$/2..$]);
        }
    }
    
    template strictSuperSetOf(U...) {
        enum strictSuperSetOf = superSetOf!U && !is(CMP!T == CMP!U);
    }
} unittest {
    assert(TypeSet!(int, string).superSetOf!(int));
    assert(TypeSet!(int, string).superSetOf!(int, string));
    assert(!TypeSet!(int, string).superSetOf!(float));
    assert(!TypeSet!(int, string).superSetOf!(float, int, string));
    assert(!TypeSet!(int, string).superSetOf!(float, int));
}
module biotronic.validation;

import std.conv : to;
import biotronic.utils;

version (unittest) {
    import std.exception : assertThrown, assertNotThrown;
}

version (D_Ddoc) {
/**
Encapsulates a validated value, the validation of which is enforced through 
$(LREF validate). $(BR)
The unadorned value is available through $(LREF value), and through alias this. 
$(BR)
The constraints can either throw on their own, or return a bool value of true 
if the constraint passed, false if it didn't. $(BR)

Example:
----
bool isPositive(int value) {
    return value >= 0;
}

void checkLessThan42(int value) {
    enforce(value < 42);
}

void foo(Validated!(int, isPositive) value) {
}

foo(13); // Refuses to compile.

Validated!(int, isPositive, checkLessThan42) x = validate!(isPositive, 
checkLessThan42)(14); // Would throw on invalid input
foo(x); // It works!
----

A validated value A whose constraints are a superset of those of another 
validated type B may be implicitly converted. The opposite is not possible.

Example:
----
alias A = Validated!(int, isPositive, checkLessThan42);
alias B = Validated!(int, isPostive);

A a = 13;
B b = a;

a = b; // Error
----

If the wrapped type is convertible, and the constraints match, a type 
conversion is performed.

Example:
----
Validated!(int, isPositive) a = validate!isPositive(4);

Validated!(long, isPositive) b = a;
----

**/
    struct Validated(T, Constraints) if (Constraints.length > 0 && 
hasNoDuplicates!Constraints) {
        /// The wrapped value.
        @property public T value() { return T.init; }
    }
}

template Validated(T, _Constraints...) if (_Constraints.length > 0 && 
!isSorted!_Constraints && hasNoDuplicates!_Constraints) {
    alias Validated!(T, StaticSort!(sortPred, _Constraints)) Validated;
}

struct Validated(T, _Constraints...) if (_Constraints.length > 0 && 
isSorted!_Constraints && hasNoDuplicates!_Constraints) {
    alias _Constraints constraints;
    
    private T _value;
    @property inout
    public inout(T) value() {
        return _value;
    }
    alias value this;
    
    @disable this();
    
    /+
    debug {
        this(int line = __LINE__, string file = __FILE__)(T other) {
            alias create = validate!constraints;
            this = create!(T, line, file)(other);
        }
    } else {
        this(T other) {
            this = validate!constraints(other);
        }
    }
    +/
    
    this(U)(U other) if (isValidated!U && 
TypeSet!(U.constraints).superSetOf!(constraints) ) {
        _value = other._value;
    }
    
    typeof(this) opAssign(U)(U other) if (isValidated!U && 
TypeSet!(U.constraints).superSetOf!(constraints) && is(typeof(_value = 
other._value))) {
        _value = other._value;
        return this;
    }
    
    inout(U) opCast(U)() inout if (isValidated!U && 
TypeSet!(constraints).superSetOf!(U.constraints) && is(typeof(other._value = 
cast(typeof(other._value))_value))) {
        U result = void;
        result._value = cast(typeof(other._value))_value;
        return result;
    }
    
    inout(U) opCast(U)() inout if (is(T : U)) {
        return value;
    }
}

template isValidated(T...) if (T.length == 1) {
    static if (is(typeof(T))) {
        enum isValidated = isValidated!(typeof(T));
    } else {
        enum isValidated = is(T[0] == Validated!U, U...);
    }
} unittest {
    assert(isValidated!(Validated!(int, isPositive)));
    assert(isValidated!(validate!(isPositive)(4)));
    assert(!isValidated!string);
    assert(!isValidated!"foo");
}

/**
validate checks that the value passes all constraints, and returns a $(LREF 
Validated).

Example:
----
void foo(Validated!(int, isPositive) value) {
}

auto a = validate!isPositive(4);
foo(a);
----

Multiple constraints may be passed to validate.

Example:
----
auto b = validate!(isPositive, checkLessThan42)(54); // Will throw at runtime.
----
**/
template validate(Constraints...) if (Constraints.length > 0) {
    auto validateImpl(string loc, T)(T value) {
        import std.exception : enforce;
        import std.typetuple : TypeTuple;
        
        foreach (fn; Constraints) {
            staticEnforce!(is(typeof(fn(value))), loc ~ "Invalid constraint " ~ 
TypeTuple!(fn).stringof[6..$-1] ~ " for value of type " ~ T.stringof);
            static if (is(typeof({if (fn(value)){}}))) {
                enforce(fn(value), loc ~ "Validation failed for value (" ~ 
value.to!string ~ "). Constraint: " ~ TypeTuple!(fn).stringof[6..$-1]);
            }
            fn(value);
        }
        
        static if (isValidated!T) {
            Validated!(typeof(T._value), NoDuplicates!(Constraints, 
T.constraints)) result = void;
        } else {
            Validated!(T, Constraints) result = void;
        }
        result._value = value;
        return result;
    }
    debug {
        auto validate(T, int line = __LINE__, string file = __FILE__)(T value) {
            return validateImpl!(file ~ "(" ~ line.to!string ~ "): ")(value);
        }
    } else {
        auto validate(T)(T value) {
            return validateImpl!""(value);
        }
    }
} unittest {
    assertNotThrown(validate!(isPositive)(3));
    assertThrown(validate!(isPositive)(-4));
}

/**
assumeValidated does not run any checks on the passed value, and assumes that 
the programmer has done so himself. This is useful when checks may be 
prohibitively expensive or in inner loops where maximum speed is required.

Example:
----
auto a = assumeValidated!isPositive(-4);
----
**/
template assumeValidated(Constraints...) if (Constraints.length > 0) {
    auto assumeValidated(T)(T value) {
        version (alwaysValidate) {
            return validate!Constraints(value);
        } else {
            Validated!(T, Constraints) result = void;
            result._value = value;
            return result;
        }
    }
} unittest {
    assertNotThrown(assumeValidated!isPositive(-2));
}

version (unittest) {
    import std.exception : enforce;
    bool isPositive(int value) {
        return value >= 0;
    }
    void checkLessThan42(int value) {
        enforce(value < 42);
    }
    void checkString(string value) {
    }
    bool notNull(int* p) {
        return p !is null;
    }
}

unittest {
    void test1(int a) {}
    void test2(Validated!(int, isPositive)) {}
    void test3(Validated!(int, isPositive, checkLessThan42)) {}
    
    Validated!(int, isPositive, checkLessThan42) a = void;
    Validated!(int, isPositive) b = void;
    Validated!(long, isPositive) r = a;
    
    // Bug 11601
    pragma(msg, "Please ignore this warning:");
    assert(!__traits(compiles, {Validated!(int, checkString) s = 
validate!checkString(3);}));
    
    a = validate!(checkLessThan42, isPositive)(3);
    b = a;
    a = validate!(checkLessThan42)(b);
    
    test1(b);
    test1(a);
    test3(a);
    assert(!__traits(compiles, test2(3)));
    assert(!__traits(compiles, test3(b)));
    
    
    Validated!(int*, notNull) n = validate!notNull(new int);
}

void main() {
}

Reply via email to