Search code examples
scalaormenumerationjooq

How to model an enum of String based static Ids in Scala?


let's say I have a reference data table roles filled with all roles that a user might be granted. The rows are quite stable, meaning that it's uncommon that someone adds a new role to the table. Additionally there's a users table and a join-table users_roles. In fact, the roles table is just required to grant a user some predefined roles by adding a record to users_roles.

The roles table is quite simple:

CREATE TABLE IF NOT EXISTS admin.roles (
  id          CHAR(16) PRIMARY KEY,
  description VARCHAR(256) NOT NULL
);

The following example describes a role:

INSERT INTO admin.roles VALUES('CS_AGENT', 'A customer service agent');

Obviously, I need the possible id values somewhere in my code. This is a set of Strings, but I'd like to prevent magic Strings and make this more type safe.

As far as I understand, there are several options:

  • create a symbol for each role id
  • create a new type RoleId that extends String and declare vals

In order to define the set of role ids, these are my options:

  • use an Enumeration
  • use a sealed trait/sealed object and derive case objects from it

I'm using JOOQ for my persistence layer, and it would be nice if I could use the type safe RoleId in my queries without manually converting it to String and vice versa.

What would be the best solution for this ?


Solution

  • I am not quite sure I get all of your problem, but would not something like this be the solution?

    /** Represents a RoleId from the roles table. */
    sealed trait RoleId {
      def name: String
      def description: String
      override final def toString: String = name
    }
    
    object RoleId {
      case object CS_AGENT extends RoleId {
        override val name = "CS_AGENT"
        override val description = "A customer service agent"
      }
      // Define all other roles...
    
      /** All roles */
      val allRoles: Set[RoleId] = Set(
        CS_AGENT,
        // All other roles...
      )
    
      /** Returns an RoleId given its name, if the name is not found this will return a None. */
      def fromString(name: String): Option[RoleId] = name.toUpperCase match {
        case "CS_AGENT" => Some(CS_AGENT)
        // All other cases..
        case _ => None
      }
    }
    

    This is completely typesafe and if you need to go to/from a String there are the toString and fromString methods.

    The only (big) downside of this approach is a lot of boilerplate code which is easy to screw up - creating a new RoleId but not adding it to the Set, typo in the name or in the case, etc.
    An alternative to fixing this is to make this file autogenerated by SBT from some kind of config (even reading the SQL table if reachable in the build environment), for that part this answer of mine to another question may help.