@ofzza/entt

Modular TypeScript data-modeling solution, implementing nestable serialization/deserialization and flexible validation

Usage no npm install needed!

<script type="module">
  import ofzzaEntt from 'https://cdn.skypack.dev/@ofzza/entt';
</script>

README

enTT

enTT, read as "Entity", is an extensible TypeScript data-modeling solution with some of the typically required functionality, such as change-detection, easy import/export, composition/decomposition, data validation, etc., all available out of the box and easy to use.

Table of contents:

Get enTT

To start using enTT in your project, simply install it from NPM by running the following in your terminal:

> npm install @ofzza/entt --save

Alternatively, get an extended implementation:

Using enTT

To make any class an enTT class, just extend the EnTT base and run super.entt() in its constructor, right after super():

import { EnTT } from '@ofzza/entt';

class MyEntityClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }
}

By doing this any properties defined in the class will now be managed and will be eligible for receiving additional functionality via a number of decorators.

 

Note: All properties, as to be picked up by EnTT base class, need to be initialized, even if this means initializing them explicitly as undefined.

EXAMPLE
public invalid1;          // Won't get picked up by EnTT and won't be eligible to accept functionality via decorators

public invalid2: string;  // Won't get picked up by EnTT and won't be eligible to accept functionality via decorators

public valid1 = undefined as string;  // Valid EnTT property

public valid2 = 'abcd' as string;     // Valid EnTT property

public valid3 = 'abcd';               // Valid EnTT property
 

@Property decorator

The @Property decorator allows for customization of properties as read-only, write-only, enumerable or non-enumerable, or for assignment of a custom getter or setter.

Enumerable

All properties are enumerable by default. To turn a property non-enumerable, just decorate it, like so:

@Property({ enumerable: false })
EXAMPLE
import { EnTT, Property } from '@ofzza/entt';

class MyEntityClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Property({ enumerable: true })
  public standard = 'default';

  @Property({ enumerable: true })
  public enumerable = 'enumerable';

  @Property({ enumerable: false })
  public nonenumerable = 'nonenumerable';
}

const instance = new MyEntityClass();
console.log(Object.keys(instance)); // Outputs: "standard", "enumerable"

Read-only / Write-only

Any property can be made read-only or write only by explicitly removing its setter or getter, like so:

@Property({ set: false })

@Property({ get: false })
EXAMPLE
import { EnTT, Property } from '@ofzza/entt';

class MyEntityClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Property({ set: false })
  public readonly = 'readonly';

  @Property({ get: false })
  public writeonly = 'writeonly';
}

const instance = new MyEntityClass();
instance.readonly = 'value'; // Throws error
console.log(instance.readonly); // Outputs: "readonly"
instance.writeonly = 'value'; // Accepts value
console.log(instance.writeonly); // Outputs: undefined

Custom getter and/or setter

Any property can be augmented with a custom setter and/or getter modifying the property value before reading and/or writing, like so:

@Property({ set: (value, obj) => any })

@Property({ get: (value, obj) => any })
EXAMPLE
import { EnTT, Property } from '@ofzza/entt';

class MyPersonClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Property({ set: (value, obj) => toTitleCase(value) })
  public firstName = undefined as string;

  @Property({ set: (value, obj) => toTitleCase(value) })
  public lastName = undefined as string;

  @Property({ get: (value, obj) => `${obj.firstName} ${obj.lastName}` })
  public fullName = undefined as string;
}

const instance = new MyPersonClass();
instance.firstName = 'john';
console.log(instance.firstName); // Outputs: "John"
instance.lastName = 'doe';
console.log(instance.lastName); // Outputs: "Doe"
console.log(instance.fullName); // Outputs: "John Doe"

Property tagging

Properties can be tagged with a single or multiple string tags. Later, properties can be searched by tag. This can be useful when writing a service that needs to accept different EnTT models and somehow know which properties serve a certain purpose on each, like finding a PrimaryKey property, or a property best used to represent a name of the model instance. Property tagging is used like so:

@Property({ tag: 'PK' })
public id1 = undefined as string;

...

const keys1 = MyEntityClass.findTaggedProperties('PK');

...

@Property({ tag: ['PK', 'guid'] })
public id2 = undefined as string;

...

const keys2 = EnTT.findTaggedProperties('PK', { from: MyEntityClass });

EXAMPLE
import { EnTT, Property } from '@ofzza/entt';

class MyPersonClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Property({ tag: 'callsign' })
  public name = undefined as string;
}

class MyCarClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Property({ tag: 'callsign' })
  public make = undefined as string;
}

function promptCallsign(instance: EnTT, from: new () => EnTT) {
  return instance[EnTT.findTaggedProperties('callsign', { from })[0]];
}

const person = new MyPersonClass();
person.name = 'Marty McFly';
const car = new MyCarClass();
car.make = 'Delorean';

console.log(promptCallsign(person, MyPersonClass)); // Outputs: Marty McFly
console.log(promptCallsign(car, MyCarClass)); // Outputs: Delorean

@Serializable decorator

The @Serializable decorator provides a simple way of serializing and deserializing EnTT instances into and from raw data, such that even nested instances will be preserved.

All EnTT instances will expose serialization methods:

class MyEntityClass extends EnTT {

  public serialize (type = 'object' as 'object'|'json') => object|string

  public deserialize (value: object|string, type = 'object' as 'object'|'json', { validate = true }) => void

  public static cast (value: object|string, { into = undefined as ((new () => EnTT) | (new () => EnTT)[] | Record<any, (new () => EnTT)>), type = 'object' as ('object'|'json'), validate = true } = {}) => MyEntityClass

  public static clone (instance: MyEntityClass, { target: MyEntityClass, validate = true }) => MyEntityClass

}

Simply serialize, deserialize, cast and clone

EXAMPLE

Without any customization, all properties can be serialized and deserialized:

import { EnTT } from '@ofzza/entt';

class MyPersonClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  public firstName = undefined as string;
  public lastName = undefined as string;
}

const instance = new MyPersonClass();
instance.firstName = 'John';
instance.lastName = 'Doe';

const serialized = instance.serialize();
console.log(serialized); // Outputs: { firstName: "John", lastName: "Doe" }

const deserialized = new MyPersonClass();
deserialized.deserialize(serialized);
console.log(deserialized.firstName); // Outputs: "John"
console.log(deserialized.lastName); // Outputs: "Doe"

const castSingle = MyPersonClass.cast(serialized);
console.log(castSingle instanceof MyPersonClass); // Outputs: true
console.log(castSingle.firstName); // Outputs: "John"
console.log(castSingle.lastName); // Outputs: "Doe"

const castArray = MyPersonClass.cast([serialized, serialized, serialized], {
  into: [MyPersonClass],
});
console.log(castArray[0] instanceof MyPersonClass); // Outputs: true
console.log(castArray[0].firstName); // Outputs: "John"
console.log(castArray[0].lastName); // Outputs: "Doe"

const castHashmap = MyPersonClass.cast({ a: serialized, b: serialized, c: serialized }, { into: { MyPersonClass } });
console.log(castHashmap.a instanceof MyPersonClass); // Outputs: true
console.log(castHashmap.a.firstName); // Outputs: "John"
console.log(castHashmap.a.lastName); // Outputs: "Doe"

const castPromise = await MyPersonClass.cast(Promise.resolve(serialized));
console.log(castPromise instanceof MyPersonClass); // Outputs: true
console.log(castPromise.firstName); // Outputs: "John"
console.log(castPromise.lastName); // Outputs: "Doe"

const cloned = MyPersonClass.clone(instance);
console.log(instance !== cloned); // Outputs: true
console.log(instance.serialize('json') === cloned.serialize('json')); // Outputs: true

Aliasing property names

Any property can define an alias to be used when serializing and deserializing data, like so:

@Serializable({ alias: string })
EXAMPLE
import { EnTT } from '@ofzza/entt';

class MyPersonClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Serializable({ alias: 'first_name' })
  public firstName = undefined as string;

  @Serializable({ alias: 'last_name' })
  public lastName = undefined as string;
}

const instance = new MyPersonClass();
instance.firstName = 'John';
instance.lastName = 'Doe';

const serialized = instance.serialize();
console.log(serialized); // Outputs: { first_name: "John", last_name: "Doe" }

const deserialized = new MyPersonClass();
deserialized.deserialize(serialized);
console.log(deserialized.firstName); // Outputs: "John"
console.log(deserialized.lastName); // Outputs: "Doe"

const cast = MyPersonClass.cast(serialized);
console.log(cast.firstName); // Outputs: "John"
console.log(cast.lastName); // Outputs: "Doe"

Custom serialization

Any property can be defined not participating in serialization, like so:

@Serializable({ serialize: false })

... not participating in de-serialization, like so:

@Serializable({ deserialize: false })

... or both, like so:

@Serializable({ serialize: false, deserialize: false })
EXAMPLE
import { EnTT } from '@ofzza/entt';

class MyAuthenticationClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Serializable()
  public password = undefined as string;

  @Serializable({ serialize: false, deserialize: false })
  public repeatPassword = undefined as string;
}

const instance = new MyAuthenticationClass();
instance.password = '123';
instance.repeatPassword = '123';

const serialized = instance.serialize();
console.log(serialized); // Outputs: { password: '123' }

Any property can also have defined custom serialization and/or de-serialization mapping functions, like so:

@Serializable({ deserialize: (value, obj) => any })

@Serializable({ serialize: (value, obj) => any, })
EXAMPLE
import { EnTT } from '@ofzza/entt';

class MyTimestampedClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Serializable({
    deserialize: (value, obj) => new Date(value),
    serialize: (value, obj) => value.getTime(),
  })
  public timestamp = undefined as Date;
}

const now = Date.now(),
instance = new MyTimestampedClass();
instance.timestamp = new Date(now);

const serialized = instance.serialize();
console.log(JSON.stringify(serialized) === JSON.stringify({ timestamp: now }); // Outputs: true

const deserialized = new MyTimestampedClass();
deserialized.deserialize({ ...serialized });
console.log(deserialized.timestamp.getTime() === instance.timestamp.getTime()); // Outputs: true

const cast = MyTimestampedClass.cast(serialized);
console.log(cast.timestamp.getTime() === instance.timestamp.getTime()); // Outputs: true

Preserving nested class instances

Any property can hold nested instances of other EnTT classes, either directly or in an array or a hashmap. If this is the case, it is possible to configure the property as such so that when deserializing all the nested EnTT instances will be cast into their correct classes, like so:

@Serializable({ cast: MyEnTTClass })

@Serializable({ cast: [MyEnTTClass] })

@Serializable({ cast: {MyEnTTClass} })

... or using alternative syntax meant to help mitigate circular dependency problems:

@Serializable({ cast: () => MyCurcularDependencyEnTTClass })

@Serializable({ cast: () => [MyCurcularDependencyEnTTClass] })

@Serializable({ cast: () => ({MyCurcularDependencyEnTTClass}) })
EXAMPLE
import { EnTT } from '@ofzza/entt';

class MyPersonClass extends EnTT {
  constructor(name?: string) {
    super();
    super.entt();
    this.name = name;
  }

  public name = undefined as string;

  @Serializable({ cast: MyPersonClass })
  public spouse = undefined as MyPersonClass;

  @Serializable({ cast: [MyPersonClass] })
  public siblings = [] as MyPersonClass[];

  @Serializable({ cast: { MyPersonClass } })
  public parents = {
    mother: undefined as MyPersonClass,
    father: undefined as MyPersonClass,
  } as Record<string, MyPersonClass>;
}

const person = new MyPersonClass('John Doe');
person.spouse = new MyPersonClass('Joanna Doe');
person.siblings.push(new MyPersonClass('Jo Doe'), new MyPersonClass('Johnny Doe'), new MyPersonClass('Jay Doe'));
person.parents.mother = new MyPersonClass('Joanna Doe Sr.');
person.parents.father = new MyPersonClass('John Doe Sr.');

const serialized = person.serialize();
// Equals:
// {
//   name: "John Doe",
//   spouse: {
//     name: "Joanna Doe",
//     siblings: [],
//     parents: {}
//   },
//   siblings: [
//     {
//       name: "Jo Doe",
//       siblings: [],
//       parents: {}
//     },
//     {
//       name: "Johnny Doe",
//       siblings: [],
//       parents: {}
//     },
//     {
//       name: "Jay Doe",
//       siblings: [],
//       parents: {}
//     }
//   ],
//   parents: {
//     mother: {
//       name: "Joanna Doe Sr.",
//       siblings: [],
//       parents: {}
//     },
//     father: {
//       name: "John Doe Sr.",
//       siblings: [],
//       parents: {}
//     }
//   }
// }

const deserialized = new MyPersonClass();
deserialized.deserialize(serialized);
console.log(deserialized instanceof MyPersonClass); // Outputs: true
console.log(deserialized.spouse instanceof MyPersonClass); // Outputs: true
console.log(deserialized.siblings[0] instanceof MyPersonClass); // Outputs: true
console.log(deserialized.siblings[1] instanceof MyPersonClass); // Outputs: true
console.log(deserialized.siblings[2] instanceof MyPersonClass); // Outputs: true
console.log(deserialized.parents.mother instanceof MyPersonClass); // Outputs: true
console.log(deserialized.parents.father instanceof MyPersonClass); // Outputs: true

const cast = MyPersonClass.cast(serialized);
console.log(cast instanceof MyPersonClass); // Outputs: true
console.log(cast.spouse instanceof MyPersonClass); // Outputs: true
console.log(cast.siblings[0] instanceof MyPersonClass); // Outputs: true
console.log(cast.siblings[1] instanceof MyPersonClass); // Outputs: true
console.log(cast.siblings[2] instanceof MyPersonClass); // Outputs: true
console.log(cast.parents.mother instanceof MyPersonClass); // Outputs: true
console.log(cast.parents.father instanceof MyPersonClass); // Outputs: true

@Validate decorator

The @Validate decorator provides a simple way of validating properties' values and checking for invalid values across the entire EnTT instance.

All EnTT instances will expose validation methods:

class MyEntityClass extends EnTT {

  public get valid () => boolean

  public errors () => Record<string, EnttValidationError[]>

}

Validate by data type

Simplest, and very limited way of validating property values is by their type. You can specify the expected type like so:

@Validate({ type: 'boolean' })

@Validate({ type: 'number' })

@Validate({ type: 'string' })

@Validate({ type: 'object' })
EXAMPLE
import { EnTT } from '@ofzza/entt';

class MyValidClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Validate({ type: 'boolean' })
  public aBoolean = false as boolean;

  @Validate({ type: 'number' })
  public aNumber = 123 as number;

  @Validate({ type: 'string' })
  public aString = 'abc' as string;

  @Validate({ type: 'object' })
  public anObject = {} as object;
}

const instance = new MyValidClass();
console.log(instance.valid); // Outputs: true
console.log(instance.errors); // Outputs: {}

instance.aBoolean = undefined;
instance.aNumber = undefined;
instance.aString = undefined;
instance.anObject = undefined;

console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    aBoolean: [
//      new EnttValidationError({
//        message: 'Value undefined is not of required type "boolean"!'
//      })
//    ]
//    aNumber:  [
//      new EnttValidationError({
//        message: 'Value undefined is not of required type "number"!'
//      })
//    ]
//    aString:  [
//      new EnttValidationError({
//        message: 'Value undefined is not of required type "string"!'
//      })
//    ]
//    anObject: [
//      new EnttValidationError({
//        message: 'Value undefined is not of required type "object"!'
//      })
//    ]
// }

Validate using a custom validator

To validate against more complex criteria, you can use a custom validator.

Custom validator function

The most basic custom validator is just a function evaluating the validity of a property value within the context of the EnTT instance:

@Validate({ provider: (value, obj) => Error[] | Error | string | boolean }) });
EXAMPLE
import { EnTT } from '@ofzza/entt';

class MyDatesClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  // Validate year is within a predefined scope
  @Validate({ provider: (value, obj) => value > 1900 && value < 2100 })
  public born = undefined as number;

  // Validate year is within dynamic scope and throw custom validation errors
  @Validate({
    provider: (value, obj) => {
      const errs = [];
      if (value < obj.born) {
        errs.push(
          new EnttValidationError({
            type: 'custom',
            message: 'Graduation year must be greater than birth date!',
            context: {},
          }),
        );
      }
      if (value >= obj.born) {
        errs.push(
          new EnttValidationError({
            type: 'custom',
            message: 'Graduation year must be smaller than 2100!',
            context: {},
          }),
        );
      }
      return errs;
    },
  })
  public graduated = undefined as number;
}

const instance = new MyDatesClass();
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    born: [
//      new EnttValidationError({
//        message: 'Value undefined not allowed!'
//      })
//    ]
// }

instance.born = 1800;
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    born: [
//      new EnttValidationError({
//        message: 'Value 1800 not allowed!'
//      })
//    ]
// }

instance.born = 1950;
instance.graduated = 1949;
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    graduated: [
//      new EnttValidationError({
//        type: 'custom',
//        message: 'Graduation year must be greater than birth date!'
//      })
//    ]
// }

JOI validator

EnTT will also know to recognize JOI validation expressions:

import * as Joi from 'joi';
@Validate({ provider: Joi.any() });
EXAMPLE
import * as Joi from 'joi';
import { EnTT } from '@ofzza/entt';

class MyDatesClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  // Validate year is within a predefined scope
  @Validate({
    provider: Joi.number().strict().integer().min(1900).max(2100).required(),
  })
  public born = undefined as number;

  // Validate year is within dynamic scope and throw custom validation errors
  @Validate({
    provider: Joi.number().strict().integer().min(Joi.ref('$.born')).max(2100).required(),
  })
  public graduated = undefined as number;
}

const instance = new MyDatesClass();
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    born: [
//      new EnttValidationError({
//        type: 'any.required',
//        message: 'Value undefined is required'
//      })
//    ],
//    graduated: [
//      new EnttValidationError({
//        type: 'any.required',
//        message: 'Value undefined is required'
//      })
//    ]
// }

instance.born = 1800;
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    born: [
//      new EnttValidationError({
//        type: 'number.min',
//        message: 'Value 1800 must be larger than or equal to 1900'
//      })
//    ],
//    graduated: [
//      new EnttValidationError({
//        type: 'any.required',
//        message: 'Value undefined is required'
//      })
//    ]
// }

instance.born = 1950;
instance.graduated = 1949;
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    graduated: [
//      new EnttValidationError({
//        type: 'any.ref',
//        message: 'Value 1949 limit references "ref:global:.born" which must be a number'
//      })
//    ]
// }

YUP validator

EnTT will also know to recognize YUP validation expressions:

import * as Yup from 'yup';
@Validate({ provider: Yup.any() });
EXAMPLE
import * as Yup from 'yup';
import { EnTT } from '@ofzza/entt';

class MyDatesClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  // Validate year is within a predefined scope
  @Validate({
    provider: Yup.number().strict().integer().min(1900).max(2100).required(),
  })
  public born = undefined as number;

  // Validate year is within dynamic scope and throw custom validation errors
  @Validate({
    provider: Yup.number()
      .strict()
      .integer()
      .min(Yup.ref('$.born') as any)
      .max(2100)
      .required(),
  })
  public graduated = undefined as number;
}

const instance = new MyDatesClass();
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    born: [
//      new EnttValidationError({
//        type: 'required',
//        message: 'Value undefined is a required field'
//      })
//    ],
//    graduated: [
//      new EnttValidationError({
//        type: 'required',
//        message: 'Value undefined is a required field'
//      })
//    ]
// }

instance.born = 1800;
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    born: [
//      new EnttValidationError({
//        type: 'min',
//        message: 'Value 1800 must be greater than or equal to 1900'
//      })
//    ],
//    graduated: [
//      new EnttValidationError({
//        type: 'required',
//        message: 'Value undefined is a required field'
//      })
//    ]
// }

instance.born = 1950;
instance.graduated = 1949;
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    graduated: [
//      new EnttValidationError({
//        type: 'min',
//        message: 'Value 1949 must be greater than or equal to 1950'
//      })
//    ]
// }

Validate using a multiple custom validators

On top of being able to set a custom validation provider, you can also set an array of multiple custom validation providers all of which will be evaluated in turn. This allows you to mix and match between different validation methods for the same property:

import * as Joi from 'joi';
import * as Yup from 'yup';

@Validate({
  provider: [
    (value, obj) => Error[] | Error | string | boolean }),
    Joi.any(),
    Yup.any()
  ]
});
EXAMPLE
import * as Joi from 'joi';
import * as Yup from 'yup';
import { EnTT } from '@ofzza/entt';

class MyDatesClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  // Validate year is within a predefined scope
  @Validate({
    provider: [
      (value, obj) => value > 1900 && value < 2100,
      Joi.number().strict().integer().min(1900).max(2100).required(),
      Yup.number().strict().integer().min(1900).max(2100).required(),
    ],
  })
  public born = undefined as number;
}

const instance = new MyDatesClass();
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    born: [
//      new EnttValidationError({
//        message: 'Value undefined not allowed!'
//      })
//      new EnttValidationError({
//        type: 'any.required',
//        message: 'Value undefined is required'
//      }),
//      new EnttValidationError({
//        type: 'required',
//        message: 'Value undefined is a required field'
//      })
//    ],
// }

instance.born = 1800;
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    born: [
//      new EnttValidationError({
//        message: 'Value 1800 not allowed!'
//      }),
//      new EnttValidationError({
//        type: 'number.min',
//        message: 'Value 1800 must be larger than or equal to 1900'
//      }),
//      new EnttValidationError({
//        type: 'min',
//        message: 'Value 1800 must be greater than or equal to 1900'
//      })
//    ]
// }

Validating nested class instances

When nesting EnTT classes, invalid nested instances will automatically invalidate their parents.

EXAMPLE
import { EnTT } from '@ofzza/entt';

class MyNestedClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Validate({ provider: Yup.number().strict().required() })
  public aNumber = undefined as number;
}

class MyParentClass extends EnTT {
  constructor() {
    super();
    super.entt();
  }

  @Validate({ provider: Yup.boolean().strict().required() })
  public aBoolean = undefined as boolean;

  public nested = new MyNestedClass();
}

const instance = new MyParentClass();
(instance as any).aBoolean = 'abc';
((instance as any).nested as any).aNumber = 'abc';
console.log(instance.valid); // Outputs: false
console.log(instance.errors);
// Outputs: {
//    aBoolean: [
//      new EnttValidationError({
//        type: 'typeError',
//        message: 'Value "abc" must be a `boolean` type, but the final value was: `"abc"`.'
//      })
//    ],
//    nested.aNumber: [
//      new EnttValidationError({
//        type: 'typeError',
//        message: 'Value "abc" must be a `number` type, but the final value was: `"abc"`.'
//      })
//    ]
// }

Contributing

Reporting Issues

When reporting issues, please keep to provided templates.

Before reporting issues, please read: GitHub Work-Flow

Contributing Code

For work-flow and general etiquette when contributing, please see:

Please accompany any work, fix or feature with their own issue, in it's own branch (see Git Source-Control Work-Flow for branch naming conventions), and once done, request merge via pull request.

When creating issues and PRs, please keep to provided templates.