Skip to content

Enum Attributes

Bill Heaton edited this page Jan 12, 2016 · 11 revisions

Problem

Your Resource is persisted with a database that supports Enum types and your API service application supports using Enum attributes on your model, yet JavaScript does not. You would like to support storing a value that your API references as a key yet stores a human readable value for the enum.

Solution

When using the JSONAPI::Resources gem for your JSON API solution you most likely will use Rails and Active Record.

Use an enum attribute for the model class, for example in a Ruby on Rails app you model may use:

class Game < ActiveRecord::Base
  enum status: {
    active: 'Active',
    inactive: 'Inactive'
  }
  validates : status, presence: true
end

Your migration may include some SQL, like so:

class CreateGames < ActiveRecord::Migration
  def up
    execute <<-SQL
      CREATE TYPE status AS ENUM ('Active', 'Inactive');
    SQL

    enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp')
    create_table :games, id: :uuid, default: 'uuid_generate_v4()' do |t|
      t.string :name
      t.column 'status', :status
      t.timestamps null: false
    end
  end

  def down
    drop_table :games

    execute <<-SQL
      DROP TYPE status;
    SQL
  end
end

The JSONAPI::Resource for serialization:

module Api
  module V1
    class GameResource < JSONAPI::Resource
      attributes :name, :status
      key_type :uuid
    end
  end
end

Now your API will send the status attribute as snake_case and store your enum type Capitalized - active: 'Active', inactive: 'Inactive'

Your JavaScript (Ember) application will need to use a human readable value for the status attribute of your resource. (In this case the lowercase value would work but imagine if the keys were snake case and the stored values used multiple words.)

Create a Map like utility class that can lookup a value for an object. Here is an example I use:

utils/transform-map.js

import { isBlank, isType } from 'ember-jsonapi-resources/utils/is';

/**
  Abstract class to transform mapped data structures

  @class TransformMap
**/
export default class TransformMap {

  /**
    @method constructor
    @param {Object} [map] created with `null` as prototype
  **/
  constructor(map) {
    this.map = map;
    this.keys = Object.keys(map);
    let inverse = Object.create(null);
    let values = [];
    let entries = [];
    let pair;
    for (let key in map) {
      values.push(map[key]);
      inverse[map[key]] = key;
      pair = Object.create(null);
      entries.push([key, map[key]]);
    }
    Object.freeze(inverse);
    this.values = values;
    this.inverse = inverse;
    this.entries = entries;
  }

  /**
    @method lookup
    @param {String} [value]
    @parm {String} [use='keys'] keys or values
    @return {String|Null} [value] name or null
  */
  lookup(value, use = 'keys') {
    if (isBlank(value) || value === '') {
      value = null;
    } else if (isType('string', value)) {
      if (this[use].indexOf(value) > -1) {
        if (use === 'keys') {
          value = this.map[value];
        } else if (use === 'values') {
          value = this.inverse[value];
        }
      }
    }
    return (value) ? value : null;
  }
}

I like to create utility objects that I can use as dictionaries in my app. Here is a util for the status keys/values:

utils/dictionaries/status

To generate a dictionary use a blueprint:

ember g jsonapi-dictionary status active:Active inactive:Inactive
// Map of status enum values
const dictionary = Object.create(null);

dictionary["active"] = "Active";
dictionary["inactive"] = "Inactive";

export default Object.freeze(dictionary);

And use the status constant when extend the TransformMap class in a Transform:

To generate a value transform object that uses the (util) dictionary:

ember g jsonapi-transform status

transforms/status

import TransformMap from 'ember-jsonapi-resources/utils/transform-map';
import dictionary from '../utils/dictionaries/status';

class TransformStatusAttribute extends TransformMap {

  deserialize(serialized) {
    return this.lookup(serialized);
  }

  serialize(deserialized) {
    return this.lookup(deserialized, 'values');
  }

}

export default new TransformStatusAttribute(dictionary);

Define a Transforms mixin:

See the Transforms for an example of using the jsonapi-transform-mixin generator.

import Ember from 'ember';
import statusTransform from 'app-name/transforms/status';

/**
  @class TransformsMixin
*/
export default Ember.Mixin.create({

  serializeStatusAttribute(deserialized) {
    return statusTransform.serialize(deserialized);
  },

  deserializeStatusAttribute(serialized) {
    return statusTransform.deserialize(serialized);
  }
});

See the Transforms page for details on how the serializer uses transform utilities you define. The serializer expects a specific convention for method names to (de)serialize attributes.

Discussion

Rather than using Transforms only for data (de)serialization, transform modules can be use to enforce custom data types in your client application like your API server does with it's database, specifically Enum types.

In this example the Rails backend communicates enum data values as strings, in snake_case. And, the Ember application presents those data values in a human readable format; the same format that the database stores the values. There is some duplication in defining the types on both the backend and in the client application in order to provide consistency where representing the custom types.

Clone this wiki locally