I'm very much used to enumerations a la Java, where each value in an enum
is a distinct object. You can call functions on these objects, and they're generally treated as most normal objects.
Except ActionScript 3 doesn't have enumerations. So... I had to create my own.
My hard requirements:
- Compile time type-safety checking
- Target a single value via
EnumClass.VALUE
or similar constant notation - It should be possible to call functions on enumeration values
- Serializing and deserializing an enumeration value should be possible without too much work
- Getting all values in an enumeration should be easy
It'd be nice if I didn't need a whole lot of boilerplate to define an enumeration. If I must write boilerplate, then I'd prefer to write such boilerplate once - adding values should be easy. Same goes for removing values.
With that in mind, I have managed to create the following...
Base class
package {
import flash.utils.describeType;
import flash.utils.getDefinitionByName;
import flash.utils.getQualifiedClassName;
import flash.utils.getQualifiedSuperclassName;
/**
* Base class for Enumerations.
* @author Pimgd
*/
public class Enum {
private var value:uint;
private var id:String;
public function Enum(val:uint, ID:String) {
value = val;
id = ID;
}
private static function validateClassIsSubclassOf(classToCheck:Class, baseClass:Class):Boolean {
//http://stackoverflow.com/a/6681302/540837
var qn:String = getQualifiedClassName(classToCheck);
var en:String = getQualifiedClassName(baseClass);
if (qn != en) {
qn = getQualifiedSuperclassName(classToCheck);//fix case where class isn't constructed yet?
}
while(qn != "Object") {
if (qn == en) return true;
qn = getQualifiedSuperclassName(getDefinitionByName(qn));
}
return false;
}
public static function loadValuesForEnum(enumClass:Class):Array {
if (!validateClassIsSubclassOf(enumClass, Enum)) {
ErrorManager.showDevelopmentError("loadValuesForEnum(" + getQualifiedClassName(enumClass) + ") failed - provided class is not a subclass of Enum!");
return new Array();
}
var description:XML = describeType(enumClass);
var arr:Array = new Array();
var id:uint = 0;
for each ( var constant:XML in description.constant)
{
var constantName:String = [email protected]();
if (constantName == constantName.toUpperCase()) {
arr[constantName] = new enumClass(id++, constantName);
}
}
return arr;
}
public function getValue():uint {
return value;
}
public function getID():String {
return id;
}
}
}
Severity Enum - a simple marker enumeration
package {
/**
* The Severity enumeration is used to determine the severity of an Issue.
* There are three levels/categories of severity:
* - SEVERE, for instances in which the issue has to be fixed, or the Dialogue will cause SDT to crash or otherwise experience uncaught exceptions;
* - MAJOR, for instances in which the Dialogue does not display as intended.
* Since the DialogueChecker does not have the ability to read the intention of the author of a dialogue, the DialogueChecker assumes that:
* - All lines in a dialogue that are not commented out were meant to be played. (Unused lines are bad)
* - All content in a line of a dialogue was meant to be displayed. (Failed substitutes are bad)
* - All lines in a dialogue were meant to function correctly within SDT. (SDT should not fail to parse any lines)
* In the event that the application of these assumptions to a dialogue fails,
* the cause of the failure will be labeled an issue of MAJOR severity, unless the cause is already labelled as a SEVERE issue.
* - MINOR, for instances in which the issue does not entail an error or meets the requirements for SEVERE or MAJOR severity.
* Issues with a MINOR severity tend to entail grammatical mistakes, unnecessary sections of a dialogue or line, or syntactic styling of dialogue lines.
* These three levels can each be seen as MUST (Severe), SHOULD (Major) and COULD (Minor) - "be fixed".
* In case the DialogueChecker is unable to parse a dialogue or sections of a Dialogue, issues are automatically treated as SEVERE, due to the possibility of the affected sections to contain SEVERE issues.
* @author Pimgd
*/
public class Severity extends Enum {
private static const enums:Array = Enum.loadValuesForEnum(Severity);
public static const SEVERE:Severity = enums["SEVERE"];
public static const MAJOR:Severity = enums["MAJOR"];
public static const MINOR:Severity = enums["MINOR"];
public function Severity(val:uint, ID:String) {
super(val, ID);
}
public static function getAllEnums():Array {
var tempArray:Array = new Array();
for each (var severity:Severity in enums) {
tempArray[severity.getValue()] = severity;
}
return tempArray;
}
public function getIDStandardCapitalization():String {
var ownID:String = getID();
ownID = ownID.toLowerCase();
ownID = ownID.charAt(0).toUpperCase() + ownID.substr(1);
return ownID;
}
}
}
Mood enum - a more complex enumeration with various operations to get at the values
package {
/**
* Represents a mood that a character can have in SDT.
* Moods affect how a character behaves. This has been extended to dialogue functions as well.
* Dialogue writers can check for moods to allow a character to act differently on different situations.
* This enumeration represents all the possible moods that SDT has.
* @author Pimgd
*/
public class Mood extends Enum {
private static const enums:Array = Enum.loadValuesForEnum(Mood);
public static const NORMAL:Mood = enums["NORMAL"];
public static const ANGRY:Mood = enums["ANGRY"];
public static const HAPPY:Mood = enums["HAPPY"];
public static const AHEGAO:Mood = enums["AHEGAO"];//NSFW don't google
public function Mood(val:uint, ID:String) {
super(val, ID);
}
public static function fromString(id:String):Mood {
return Mood(enums[id]);
}
public static function hasEnum(id:String):Boolean {
return fromString(id) != null;
}
public static function fromMoodAttributeValue(value:String):Mood {
if (value == null) {
return null;
}
if (StringFunctions.stringStartsWith(value, "\"") && StringFunctions.stringEndsWith(value, "\"")) {
value = value.substring(1, value.length - 1);
}
var moods:Array = getAllEnums();
for (var i:uint = 0; i < moods.length; i++) {
if (moods[i].getIDStandardCapitalization() == value) {
return moods[i];
}
}
return null;
}
public static function fromTrigger(trigger:Trigger):Mood {
if (trigger != null && trigger.getType() == TriggerType.MOOD) {
return fromString(trigger.getAction().split("_")[0]);
}
return null;
}
public static function getAllEnums():Array {
var tempArray:Array = new Array();
for each (var mood:Mood in enums) {
tempArray[mood.getValue()] = mood;
}
return tempArray;
}
public function getIDStandardCapitalization():String {
var ownID:String = getID();
ownID = ownID.toLowerCase();
ownID = ownID.charAt(0).toUpperCase() + ownID.substr(1);
return ownID;
}
}
}
How this works is that, upon first usage of the class, the class itself is instantiated. The static variable enums
is initialized, which uses reflection via describeType
to get a list of fields that need to be filled with enum values. You can see this in Enum.loadValuesForEnum
. When the call returns with a string-indexed array, it's able to set the enum values from this array.
Usage example from my code in a validator:
var currentMood:Mood = null;
var checkedFor:Mood = null;
var checkingAgainstMoodAttribute:Boolean = false;
var moodAttribute:LineAttribute = lineObject.getLineAttribute("\"mood\"");
if (moodAttribute != null) {
checkedFor = Mood.fromMoodAttributeValue(moodAttribute.getValue() as String);
if (checkedFor != null) { //else failed to identify mood value? handled in checkLineOldStyle
currentMood = checkedFor;
checkingAgainstMoodAttribute = true;
}
}
var triggers:Array = lineObject.getTriggers();
for (var i:uint = 0, isize:uint = triggers.length; i < isize; i++) {
var trigger:Trigger = triggers[i] as Trigger;
var setMood:Mood = Mood.fromTrigger(trigger);
if (setMood != null) {
if (setMood == currentMood) {
issueSnippet = "";
if (checkingAgainstMoodAttribute) {
issueMessage = "Line " + lineObject.getLineNumber() + " checks for " + checkedFor.getIDStandardCapitalization() + " mood, but then also sets the mood to " + setMood.getIDStandardCapitalization() + " in the line itself." + linebreak;
issueMessage += "It's impossible (except if the user manually changed it in the brief period in between the check and the trigger) for the mood to change in between the check and the set, so this trigger could be removed." + linebreak;
} else {
issueMessage = "Line " + lineObject.getLineNumber() + " sets the mood to " + setMood.getIDStandardCapitalization() + " multiple times in a row." + linebreak;
issueMessage += "Repeated setting of the mood to the same value has no result, and thus the trigger can be removed." + linebreak;
}
issueMessage += "Line " + lineObject.getLineNumber() + ": " + line + linebreak + linebreak;
issue = new Issue(Severity.MINOR, lineObject, issueSnippet, issueMessage, "Syntax");
addIssue(issue);
issueMessage = "";
checkingAgainstMoodAttribute = false;//flip flag to allow "repeated mood setting" issue to pop up
}
currentMood = setMood;
}
}
I'd like to know if there's a better way to make enumerations or if there's an easier way to provide fromString
, getAllEnums
and hasEnum
from the Enum
class. I'm also interested in general improvements.