If you know about enums, you probably also get why they're handy and why I thought about using them for my User
model, but but this was too difficult to implement in Fluent 4.
What is an enum?
It differs between languages, but in short it's a way for a programmer to restrict the amount of values a variable can have. If we take following Swift enum, we can better show the power of this restriction:
enum UserRole {
case admin
case user
}
var userRole: UserRoles
With this enum UserRoles
the variable userRole
can only be either admin or user, and the compiler will yell at you for even considering other values.
Come OptionSets
Sometimes we need to turn on and off features individually instead of having entire and heavy roles. Meet OptionSets, a non-exlusion err.. set. We at Inuk Entertainment (I) have decided that OptionSet is the better choice for Manga.dk's CMS, and besides it allowing for more granular controle, it also plays much nicer with Fluent 4 than enums did. To make it work with Fluent 4, we have to conform it to Codable
and add our own encoding and init as following.
struct UserRights: OptionSet, Codable {
init(rawValue: UInt64) {
self.rawValue = rawValue
}
init(from decoder: Decoder) throws {
rawValue = try .init(from: decoder)
}
func encode(to encoder: Encoder) throws {
try rawValue.encode(to: encoder)
}
let rawValue: UInt64
}
And when Fluent 4 asks, our optionset is UInt64 (this allows for 64 different options to turn on and off) and will look as follows in migration:
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(name)
.field("id", .int, .identifier(auto: true))
.field("name", .string, .required)
.field("email", .string, .required)
.field("password_hash", .string, .required)
.field("userRights", .uint64, .required )
.create()
}
This migration assumes my user model, which looks like this and has the OptionSet included below.
final class User: Model, Content {
static let schema = "users"
@ID(key: "id")
var id: Int?
@Field(key: "name")
var name: String
@Field(key: "email")
var email: String
@Field(key: "password_hash")
var passwordHash: String
@Field(key: "userRights")
var userRights: UserRights
init() { }
init(id: Int? = nil, name: String, email: String, passwordHash: String, userRights: UserRights = .everyone) {
self.id = id
self.name = name
self.email = email
self.passwordHash = passwordHash
self.userRights = userRights
}
}
struct UserRights: OptionSet, Codable {
init(rawValue: UInt64) {
self.rawValue = rawValue
}
let rawValue: UInt64
func encode(to encoder: Encoder) throws {
try rawValue.encode(to: encoder)
}
init(from decoder: Decoder) throws {
rawValue = try .init(from: decoder)
}
/// This is a given user, used for default Init in User
static let everyone: Self = []
/// Anyone with this priv can change a different user
static let modUser = Self(rawValue: 1 << 1)
/// This user can edit and add books to the system
static let mangaUpload = Self(rawValue: 1 << 2)
/// Has all the rights, you shouldn't check on this, only make a user super admin, also don't use .max, it will crash the system for currently unknown reasons
static let superAdmin = Self(rawValue: 1 << 0)
}
Let's unravel a given option static let mangaUpload = Self(rawValue: 1 << 2)
. Static means it can be called without initialising the model it's part of, Self
is a way to initialise an instance of the type the variable is part of and 1 << 2
is 1 bitshifted 2 which in binary is 100
or 4 in the decimal system. So OptionSets very much prefers a value that is in the power of two, which is why it's much easier to just use bitshifting. This is because when an OptionSet has more than one value, it's "just" added, so modUser
and mangaUploader
will be 110
.
When all is set up, it's only a matter of doing checks of wether the user has a given permissiong with myUser.userRights.contains(.modUser)
if I want to see if the user can mod a different user.