Skip to content

Latest commit

 

History

History
582 lines (458 loc) · 23.4 KB

File metadata and controls

582 lines (458 loc) · 23.4 KB

Overview

This is a file that describes the use of enum.mjs in it's current raw form, as if you simpy inserted it into your codebase.
To avoid repeating myself, this will be a verbose recap of what my code does, while my codebase combined with jsdoc will tell you how.

Contents:

Enum

It's a parent global Enum class (javascript already has Object, String etc.), used to create new enums.
In the case of this feature prototype, it is not only constistently enumerable but also iterable.
Different types of enums provide different benefits, but all of them are immutable.
If you are questioning my design decisions, here are my sources:

Note

When I say "like in x language", I mean I borrowed some ideas from the language, and made it fit into javascript.
I have been writing vanilla javascript for a long time now.
I don't posess the same level of expertise to implement an exact copy of the Enum from another language.
And either way - I don't have to do that.

Prototype

Public properties:

  • Static:

    1. .flags - control tag1, enables creation of Enum structured as a collection of flags
    2. .symbolic - control tag1, enables creation of Enum with EnumVariants that hold values
    3. .simple - control tag1, default, enables creation of a simple C style Enum
    4. .types - table of transformators for types2 used in the schema3 of EnumVariants
    5. .check() - validator function for the keys of a structure object4, fails if:
      • the keys cannot be considered valid variable names5 (e.g. const 57 or <obj>.*fish)
      • or they collide with already existing properties (e.g <enum>['[[metadata]]'] or <obj>.constructor)
    6. .match(callbacks, <EnumVariant>) - pattern matching6 function, imitates the match expression in Rust
      • it accepts callbacks for all possible variants and runs the appropriate function once it finds the match
      • the function will be given the value of the matched variant
      • throws if you do not have a function for every possible variant
    7. .checkType() - checks if a string represents one of the types2 used in a schema3
  • Instance:

    1. .values(<int>) - returns an array of names matching the bitmask7
      • <arr>.print returns a CCS (comma separated string) of all values
      • Enum.simple and Enum.flags only
    2. .keys(<int>) - returns an array of integers matching the bitmask7
      • <arr>.print returns a CCS of all keys
      • Enum.simple and Enum.flags only
    3. .entries(<int>) - returns an array of [value:key] pairs matching the bitmask7
      • <arr>.print returns a CCS of all entries, formatted as <value>(<key>),
      • Enum.simple and Enum.flags only
    4. <enum>.match = - non-enumerable, sets a default object of callbacks for pattertn matching6 on this Enum instance
      • Enum.symbolic only
    5. <enum>.match() - shortcut to Enum.match(), that uses default callbacks
      • Enum.symbolic only
    6. <enum>.size - same as Object.keys(<enum>).length, may be useful for bitmasking7
      • Enum.simple and Enum.flags only

Private properties:

  • Instance:

    1. .#form - object of default callbacks for pattern matching6

Initialization

Important

Enum can only be initialized with a {} object, it does not accept level-0 arrays
(i.e. array as a structure object4 of the enum)

EnumVariant

A subclass of Enum designed to serve as a constructor in the fields of Enum.symbolic.

Prototype

Public properties:

  • Static:
    1. .buildSchema(<obj>, <enum>) - function to parse the structure object4 and return a schema3
    2. .applySchema(<obj>, <schema>) - function to parse the source object10 and return a converted object copy
  • Instance:
    1. <enumvar>.match = - non-enumerable, sets new matching function for the relevant EnumVariant
      • not available on constructed variant instances

Private properties:

  • Inherited

Initialization

Caution

Manually creating EnumVariants can lead to unexpected problems!
❌ Do not create new EnumVariant()
✔️ Do use bound builder() functions placed in the Enum
Can use static methods of class EnumVariant

An EnumVariant is either prebuilt or constructed by invoking an enum field closure with <enum>.<prop>(<val>)

Structure

The structure of the EnumVariant is decided at the time of Enum declaration.

  • .value is a default field used for single value Enum fields
  • if the enum field contained a complex value11 at the time of declaration
    • all instances will follow it's shape and types2
    • .value will not be added by default
  • EnumVariant can handle both {} and [] objects

bound builder()

If an Enum field contained a valid type2 or a complex value11 a bound constructor function builder() is placed there, otherwise an EnumVariant containing a Symbol is placed.

  • <builder>.schema - a copy of the schema3 used by the builder
  • <builder>.match = - updates the default matching callback for the relevant EnumVariant

[[metadata]]

Is a non-enumerable field (object) present in every Enum and EnumVariant.
It contains mechanically important information and changes structure based on the kind of Enum.

  • metadata.title and metadata.type occur in all types of Enum
  • metadata.parent occurs in all instances of EnumVariant

It is named this way for 2 reasons:

  1. that's how runtime-only properties look in dev tools (i.e. [[PrimitiveValue]] of a Number)
  2. it avoids collisions since [ is an invalid character for a variable name

Enum.simple

A simple C style enum, each field gets an integer value associated with it.

Usage

Its main purpose is to allow programmers to use readable constants, that translate to a primitive integer value (compiler benefit).
It also allows you to define fusions (i.e. 5 or 101 is a fusion of 4|1).

Syntax

Initialization

To create an Enum give it:

  1. a name string,
  2. a structure object4,
  3. undefined or Enum.simple

Property access

To access any field in the enum:

  • write enum.<prop>
    • manual access by field name is designed to imitate C# casting field to integer, via enum.<prop>.int
  • or enum[<int>]
    • manual access by integer is is designed to imitate C# casting enum to integer, via enum[<int>]
  • or enum[enum.<prop>.int], but that is unnecessary, and this format can just keep getting nested

Structure

The structure object4 is only accepted with fields that pass the validation, and any value that is not a valid number is ignored.
By default each field's integer is assigned automatically, based on the order of insertion.
You can override the integer value with a valid number. Example:

const daysOfWeek = new Enum('daysOfWeek', {
  Monday: '',
  Tuesday: 0,
  Wednesday: Infinity,
  Thursday: { a: true, b: 78 },
  Friday: (c) => {
    console.log(c);
  },
  Saturday: -57,
  Sunday: '4'
});
  1. Monday is automatically assigned 0
  2. Tuesday manually takes the spot of Monday, Monday is moved to 1
  3. every other field is automatically assigned numbers 2-6
  4. except that Saturday is assigned -57 and therefore 5 doesn't exist

A sorting mechanism will ensure that you do not create duplicate integer associations, and do not overwrite any of the fields.

Every field in the enum becomes a string value. For each field, 2 properties are defined on the resulting enum:

  • enum.<prop> is non-enumerable, and will contain a String(<prop>)
    • that string object will also posess an .int property
  • enum[<int>] is enumerable, and will contain a primitive string of <prop>
    • this way integers are keys, and field names are values in any enumeration operation (i.e. Object.keys())
    • the enum will be enumerated in the correct order, independent of the field names

Behavior

log(daysOfWeek.Monday); // String('Monday')
log(daysOfWeek.Monday.int); // 1
log(daysOfWeek[0]); // Tuesday
for (const day in daysOfWeek) {
  log(day); // 0,1,2,3,4,6,-57
}
for (const val of daysOfWeek) {
  log(val); // Tuesday ... Saturday
  log(daysOfWeek[val]); // String('Tuesday') ... String('Saturday')
  log(daysOfWeek[val].int); // 0 ... -57
}

Enum.flags

A C# [Flags] style enum structured as a collection of flags.

Usage

It is similar to Enum.simple, but its main purpose is to have clearly defiend, individual bit fields.
It is structured as a collection of flags.
It also allows you to define supersets (i.e. 8 is one bit 1000, whilst 7 is a superset 0111 of bits 4|2|1)

Syntax

Initialization

To create an Enum give it:

  1. a name string,
  2. a structure object4,
  3. Enum.flags

Property access

To access any field in the enum:

  • write enum.<prop>
    • manual access by field name returns an integer that is always a standalone flag
  • or enum[<int>]
    • manual access by integer is is designed to imitate C# casting enum to integer, via enum[<int>]

Structure

The structure object4 is only accepted with fields that pass the validation, and any value is ignored12. Each field's integer is assigned automatically, based on the order of insertion.
Consider example similar to the previous object:

const daysOfBits = new Enum(
  'daysOfBits',
  {
    0: (a) => {
      return `oops, empty for bitmask ${a}`;
    },
    Monday: '',
    Tuesday: 0,
    Wednesday: Infinity,
    Thursday: { a: true, b: 78 },
    Friday: (c) => {
      console.log(c);
    },
    Saturday: -57,
    Sunday: null
  },
  Enum.flags
);
  1. Monday is automatically assigned 1
  2. Tuesday is automatically assigned 2
  3. Thursday is automatically assigned 8
  4. enum integers are different bitshifts of 1 that end at -2147483648
    • this is done to simplify bit loops and individual bit access, since you can simply write 1<<i
    • -2147483648 is simply a negative of the 32nd bit
    • the enum is limited to only 32 fields because javacript bitwise operators only deal with 32bit numbers
  5. value from the field 0 in the structure object is not ignored
    • it is expected to be a function
    • the resulting enum will contain a non-enumerable property 0
    • by default <enum>[0]() will be a function that returns undefined

Every field in the enum becomes a string value. For each field, 2 properties are defined on the resulting enum:

  • enum.<prop> is non-enumerable, and will contain a number primitive8 of <int>
  • enum[<int>] is enumerable, and will contain a string primitive8 of <prop>
    • this way integers are keys, and field names are values in any enumeration operation (e.g. Object.keys())

Behavior

log(daysOfBits.Monday); // 1
log(daysOfBits[8]); // Thursday
log(daysOfBits.entries(18 & 7)); // ['Tuesday']
for (const day in daysOfBits) {
  log(day); // 1,2,4,8,16,32,64
  log(daysOfBits[day]); // Monday ... Sunday
}
log(daysOfBits.values(1 << 26)); // oops, empty for bitmask 67108864

Enum.symbolic

A Rust style enum that doesn't have integer associations and instead holds constructors or identifiers.
This particular enum was much too complicated to implement so it likely doesn't work the same way as in Rust.

Usage

Its main purpose is to introduce a collection of constructors/signature values, that allow it to hold different values and pattern match6.

Syntax

Initialization

To create an Enum give it:

  1. a name string,
  2. a structure object4,
  3. Enum.symbolic

Property access

Property access it a little bit more complicated.

  • fields with a unique identifier
    • such field will be an EnumVariant accessible on the Enum itself
    • to access the value write <enum>.<prop>.value
  • fields with a single value constructor
    • first you will have to construct an EnumVariant instance via <enum>.<prop>(<val>)
    • to access the value write <instance>.value
      • you can also access the constructor's schema3 via <enum>.<prop>.schema
  • fields with a complex11 value constructor
    • first you will have to construct an EnumVariant instance via <enum>.<prop>(<val>)
    • such instances do not have a default field .value
    • to access any value you can ask for any property that was on the structure object4
      • you can also access the constructor's schema3 via <enum>.<prop>.schema

Structure

The structure object4 is only accepted with fields that pass validation, and the values are parsed.
Keep in mind that I had to adjust the idea of this kind of enum to play nice with javascript.
Here is an example from the rust-by-example page, with some modifications:

const WebEvent = new Enum(
  'WebEvent',
  {
    PageLoad: 'constructor',
    PageUnload: 1,
    KeyPress: 'char',
    Paste: 'string',
    Click: { x: 'int', y: 'int', z: [null, 'bool', Symbol(`should be a str`)] }
  },
  Enum.symbolic
);

Each field will eventually be an EnumVariant.
It can either be prebuilt, or await input to create a new instance.
Let's break down the different kinds of fields you can have:

  • Unique identifier, used for references:
    A field in the enum may be treated as just a constant. This is the case when you declare a field with a value that is neither an object nor a type2.
    Example:
    1. WebEvent.PageLoad was declared with an invalid value
    2. the field name is used to create a Symbol
      • this way 2 identical fields from 2 diferent enums are not equal, but their descriptions are
    3. WebEvent.PageLoad is now a prebuilt EnumVariant
  • Single value constructor:
    A field in the enum can be declared with just a type2.
    In that case, it becomes a constructor of single values, usually primitive8.
    Example:
    1. WebEvent.KeyPress was declared with 'char', which is one of the available types2
    2. schema3 is built for the singular type annotation
      • it looks like this { value: 'char' }
    3. WebEvent.KeyPress is now a bound builder()
      • it will accept a value and, in standard javascript fashion, try to convert it
      • this will create an instance of EnumVariant
      • you can keep creating more WebEvent.KeyPress instances
  • Complex constructor:
    A field in the enum can be declared with an object containing one or more fields.
    In that case, it becomes a constructor of objects.
    Each fields value is expected to be either a type2 or an object; in case it is neither, it's treated as a 'str' type2
    Example:
    1. WebEvent.Click is declared with an object
    2. the object is recursively processed and a schema3 is built
      • it loks like this { x: 'int', y: 'int', z: ['str', 'bool', 'str'] }
    3. WebEvent.Click is now a bound builder()
      • it will accept an object of values and, in standard javascript fashion, try to convert them
      • this will create an instance of EnumVariant
      • you can keep creating more WebEvent.Click instances

Behavior

const load = WebEvent.PageLoad;
const unload = WebEvent.PageUnload;
const press = WebEvent.KeyPress('xfgh');
const paste = WebEvent.Paste('hello');
const click = WebEvent.Click({
  x: 59.92,
  y: '77.1',
  z: [false, true, { location: 'here' }]
});

Here are the resulting values, all of which are instances of EnumVariant:

  1. load
    • load.value is a Symbol(PageLoad)
  2. unload
    • unload.value is a Symbol(PageUnload)
  3. press
    • press.value is 'x'
  4. paste
    • paste.value is a String('hello')
  5. click
    • does not have a default property .value but could have .value if declared by the user
    • .x is 59
    • .y is 77
    • .z is an array of ['false', true, '[object Object]']

Advanced

Enum.flags

const permissions = new Enum('permissions', {
  Read: 1,
  Delete: 2,
  Write: 4,
  Mod: 5,
  Admin: 7
});

const permissionsBits = new Enum(
  'permissionsBits',
  {
    Read: 1,
    Delete: '',
    Write: true,
    Mod: 5,
    Admin: 99
  },
  Enum.flags
);

log(permissions[permissions.Read.int | permissions.Write.int]); // Mod
log(permissions[permissions.Read.int | permissions.Write.int | permissions.Delete.int]); // Admin
log(
  permissionsBits[1 + (permissionsBits.Read | permissionsBits.Write | permissionsBits.Delete)] // Mod
);
const mod = permissionsBits.Read | permissionsBits.Write; // 5
log(permissions.values(7)); // ['Read, Delete, Write']
log(permissions.entries(7).print); // 'Read, Delete, Write'
log(permissionsBits.keys(mod)); // [1, 4]
log(permissionsBits.values(1 << 15)); // undefined

// Admin is it's own bit, Admin-1 is a superset of preceding bits
const admin =
  permissionsBits.Read | permissionsBits.Delete | permissionsBits.Write | permissionsBits.Mod; // 15
const allFlags = permissionsBits.size ** 2 - 1; // 24
const adminBit = (permissionsBits.size - 1) ** 2 - 1; // 15
log(adminBit === permissionsBits.Admin - 1); // true

if ((adminBit | permissionsBits.Delete) === permissionsBits.Admin - 1) {
  // Admin-1 contains Delete bit so adding it should change nothing
  log('is Admin');
  if ((admin & allFlags) === allFlags) {
    // Admin or Admin-1 does not stand above Admin
    // No one field can represent all flags of the Enum
    log('is Above All');
  }
} else {
  log('is Mod');
}

Enum.symbolic

WebEvent.match = {
  PageLoad(val) {
    log(val.description);
    return 8;
  },
  PageUnload(val) {
    log(val.description);
  },
  KeyPress(val) {
    log(val);
  },
  Paste(val) {
    log(val);
  },
  Click(obj) {
    log(obj.z);
  }
};

log(WebEvent.Click.schema); // {x: 'int', y: 'int', z: ['str', 'bool', 'str']}
log(WebEvent.match(load)); // logs PageLoad, returns and logs 8
WebEvent.PageLoad.match = (v) => {
  log(v.description + 5886);
};
log(WebEvent.match(load)); // logs PageLoad5886, returns and logs undefined
WebEvent.Click.match = (obj) => {
  return obj.x;
};
log(WebEvent.match(click)); // 59
for (const key in click) {
  log(key); // x, y, z
}
for (const val of click) {
  log(val); // 59, 77, ['false', true, '[object Object]']
}

Afterword

The details of methods and structures here described are pretty self explanatory.
Once you look at the code, and run examples.js a couple of times with debugger, you'll get it.
Feedback on the syntax, memory efficiency, bugs is welcome in the issues section.
I will deploy it as a package, but consider this an experiment, because I've had to take some roundabouts:

  • for convenience's sake I decided to implement it using latest features like private and static class fields from ES 2022
  • I had to "manifest" certain data that should ideally be dealt with by the ts compiler or by the runtime
    • I tried mimicking your average enum syntax best I could, but it's not completely convenient
    • [[metadata]].name exists simply for debugging purposes as the code can't read itself and know how you named the newly created Enum
    • metadata object in general is a form of introspection, necessary for a functional "feature prototype"
    • one or more loops are often used to parse the Enum structure object4 and then to parse source objects10

However I encourage people to try out this Enum implementation.
I believe this code can be rewritten to fit old enough versions of ECMAScript standard.
This project is under the permissive free license.

Footnotes

  1. Control tags are symbols used to change the functionality and output of the Enum constructor 2 3

  2. Types in a schema3 tell the EnumVariant constructor how to transform values passed in 2 3 4 5 6 7 8 9 10 11

  3. Schema is an object that acts as the blueprint for new EnumVariants, it defines the structure and the types2 of the values 2 3 4 5 6 7 8 9 10

  4. Structure object is the input object used to define the layout and values of the new Enum or layout and types2 of the new EnumVariant 2 3 4 5 6 7 8 9 10 11 12

  5. By variable naming rules I mean legal syntax, for more detail see Identifiers

  6. Enum.match() function works like a switch statement, that is an expression, that forces you to consider every possible EnumVariant case for a certain Enum 2 3 4

  7. A number can be used as a set of binary flags where bit 1/0 is true/false. In this case each bit corresponds to an individual field in the Enum (e.g 11 is binary 1011 or fourth, second, first fields) 2 3 4 5

  8. Primitive value is a non-extensible value, without properties, which is not an instance. Example:

    • typeof 'hello' === 'string' is true and 'hello' instanceof String is false
    • typeof new String('hello') === 'string' is false and new String('hello') instanceof String is true
    • FTPS9 we can ignore the fact that javascript primitives are not fully primitive, for reference
    • FTPS9 Symbols act similarly to primitives
    2 3 4
  9. For This Project's Scope - for the logic, functionality, use case, and general idea behind this code 2

  10. Source object is similar to the structure object4 but it's values are transformed according to the schema3 and a new EnumVariant instance is built 2

  11. Complex value is the opposite of a primitive8, usually an object or function 2 3

  12. field 0 is reserved for a default action from bitmasking7 methods (i.e. <enum>.values()) if they fail to find a match