GenerateToString

GenerateToString is a mixin string that automatically generates toString functions, both sink-based and classic, customizable with UDA annotations on classes, members and functions.

enum string GenerateToString;

Examples

When used with objects, toString methods of type string toString() are also created.

class Class
{
    mixin(GenerateToString);
}

(new Class).to!string.shouldEqual("Class()");
(new Class).toString.shouldEqual("Class()");

A trailing underline in member names is removed when labeling.

struct Struct
{
    int a_;
    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct(a=0)");

The @(ToString.Exclude) tag can be used to exclude a member.

struct Struct
{
    @(ToString.Exclude)
    int a;
    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct()");

The @(ToString.Optional) tag can be used to include a member only if it's in some form "present". This means non-empty for arrays, non-null for objects, non-zero for ints.

import std.typecons : Nullable, nullable;

class Class
{
    mixin(GenerateToString);
}

struct Test // some type that is not comparable to null or 0
{
    mixin(GenerateToString);
}

struct Struct
{
    @(ToString.Optional)
    int a;

    @(ToString.Optional)
    string s;

    @(ToString.Optional)
    Class obj;

    @(ToString.Optional)
    Nullable!Test nullable;

    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct()");
Struct(2, "hi", new Class, Test().nullable).to!string
    .shouldEqual(`Struct(a=2, s="hi", obj=Class(), nullable=Test())`);
Struct(0, "", null, Nullable!Test()).to!string.shouldEqual("Struct()");

The @(ToString.Optional) tag can be used with a condition parameter indicating when the type is to be _included._

struct Struct
{
    @(ToString.Optional!(a => a > 3))
    int i;

    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct()");
Struct(3).to!string.shouldEqual("Struct()");
Struct(5).to!string.shouldEqual("Struct(i=5)");

The @(ToString.Include) tag can be used to explicitly include a member. This is intended to be used on property methods.

struct Struct
{
    @(ToString.Include)
    int foo() const { return 5; }
    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct(foo=5)");

The @(ToString.Unlabeled) tag will omit a field's name.

struct Struct
{
    @(ToString.Unlabeled)
    int a;
    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct(0)");

Parent class toString() methods are included automatically as the first entry, except if the parent class is Object.

class ParentClass { mixin(GenerateToString); }

class ChildClass : ParentClass { mixin(GenerateToString); }

(new ChildClass).to!string.shouldEqual("ChildClass(ParentClass())");

Inclusion of parent class toString() can be prevented using @(ToString.ExcludeSuper).

class ParentClass { }

@(ToString.ExcludeSuper)
class ChildClass : ParentClass { mixin(GenerateToString); }

(new ChildClass).to!string.shouldEqual("ChildClass()");

The @(ToString.Naked) tag will omit the name of the type and parentheses.

@(ToString.Naked)
struct Struct
{
    int a;
    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("a=0");

Fields with the same name (ignoring capitalization) as their type, are unlabeled by default.

struct Struct1 { mixin(GenerateToString); }

struct Struct2
{
    Struct1 struct1;
    mixin(GenerateToString);
}

Struct2.init.to!string.shouldEqual("Struct2(Struct1())");

This behavior can be prevented by explicitly tagging the field with @(ToString.Labeled).

struct Struct1 { mixin(GenerateToString); }

struct Struct2
{
    @(ToString.Labeled)
    Struct1 struct1;
    mixin(GenerateToString);
}

Struct2.init.to!string.shouldEqual("Struct2(struct1=Struct1())");

Fields of type 'SysTime' and name 'time' are unlabeled by default.

struct Struct { SysTime time; mixin(GenerateToString); }

Struct strct;
strct.time = SysTime.fromISOExtString("2003-02-01T11:55:00Z");

// see unittest/config/string.d
strct.to!string.shouldEqual("Struct(2003-02-01T11:55:00Z)");

Fields named 'id' are unlabeled only if they define their own toString().

struct IdType
{
    string toString() const { return "ID"; }
}

struct Struct
{
    IdType id;
    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct(ID)");

Otherwise, they are labeled as normal.

struct Struct
{
    int id;
    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct(id=0)");

Fields that are arrays with a name that is the pluralization of the array base type are also unlabeled by default, as long as the array is NonEmpty. Otherwise, there would be no way to tell what the field contains.

import boilerplate.conditions : NonEmpty;

struct Value { mixin(GenerateToString); }
struct Entity { mixin(GenerateToString); }
struct Day { mixin(GenerateToString); }

struct Struct
{
    @NonEmpty
    Value[] values;

    @NonEmpty
    Entity[] entities;

    @NonEmpty
    Day[] days;

    mixin(GenerateToString);
}

auto value = Struct(
    [Value()],
    [Entity()],
    [Day()]);

value.to!string.shouldEqual("Struct([Value()], [Entity()], [Day()])");

Fields that are not NonEmpty are always labeled. This is because they can be empty, in which case you can't tell what's in them from naming.

import boilerplate.conditions : NonEmpty;

struct Value { mixin(GenerateToString); }

struct Struct
{
    Value[] values;

    mixin(GenerateToString);
}

Struct(null).to!string.shouldEqual("Struct(values=[])");

GenerateToString can be combined with GenerateFieldAccessors without issue.

struct Struct
{
    import boilerplate.accessors : ConstRead, GenerateFieldAccessors;

    @ConstRead
    private int a_;

    mixin(GenerateFieldAccessors);

    mixin(GenerateToString);
}

Struct.init.to!string.shouldEqual("Struct(a=0)");

Meta