summaryrefslogtreecommitdiff
path: root/lib/declarative_enum.rb
blob: 8dea9d6130bafa85c19ed2f052c59440bbfd6efd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# frozen_string_literal: true

# Extending this module will give you the ability of defining
# enum values in a declarative way.
#
#   module DismissalReasons
#     extend DeclarativeEnum
#
#     key  :dismissal_reason
#     name 'DismissalReasonOfVulnerability'
#
#     description <<~TEXT
#       This enum holds the user selected dismissal reason
#       when they are dismissing the vulnerabilities
#     TEXT
#
#     define do
#       acceptable_risk value: 0, description: 'The vulnerability is known but is considered to be an acceptable business risk.'
#       false_positive value: 1, description: 'An error in reporting the presence of a vulnerability in a system when the vulnerability is not present.'
#       used_in_tests value: 2, description: 'The finding is not a vulnerability because it is part of a test or is test data.'
#     end
#
# Then we can use this module to register enums for our Active Record models like so,
#
#   class VulnerabilityFeedback
#     declarative_enum DismissalReasons
#   end
#
# Also we can use this module to create GraphQL Enum types like so,
#
# module Types
#   module Vulnerabilities
#     class DismissalReasonEnum < BaseEnum
#       declarative_enum DismissalReasons
#     end
#   end
# end
#
# rubocop:disable Gitlab/ModuleWithInstanceVariables
module DeclarativeEnum
  # This `prepended` hook will merge the enum definition
  # of the prepended module into the base module to be
  # used by `prepend_mod_with` helper method.
  def prepended(base)
    base.definition.merge!(definition)
  end

  def key(new_key = nil)
    @key = new_key if new_key

    @key
  end

  def name(new_name = nil)
    @name = new_name if new_name

    @name
  end

  def description(new_description = nil)
    @description = new_description if new_description

    @description
  end

  def define(&block)
    raise LocalJumpError, 'No block given' unless block

    @definition = Builder.new(definition, block).build
  end

  # We can use this method later to apply some sanity checks
  # but for now, returning a Hash without any check is enough.
  def definition
    @definition.to_h
  end

  class Builder
    KeyCollisionError = Class.new(StandardError)

    def initialize(definition, block)
      @definition = definition
      @block = block
    end

    def build
      instance_exec(&@block)

      @definition
    end

    private

    def method_missing(name, *arguments, value: nil, description: nil, &block)
      key = name.downcase.to_sym
      raise KeyCollisionError, "'#{key}' collides with an existing enum key!" if @definition[key]

      @definition[key] = {
        value: value,
        description: description
      }
    end
  end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables