DEV: Add helpers for sanitizing input

DEV: Add helpers for sanitizing input

diff --git a/lib/typed_data.rb b/lib/typed_data.rb
new file mode 100644
index 0000000..4bbfabe
--- /dev/null
+++ b/lib/typed_data.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module TypedData
+  module TypedStruct
+    def self.new(base=Object, **attributes, &blk)
+      ordered_attribute_keys = attributes.keys.sort
+
+      Class.new(base) do
+        attr_reader *attributes.keys
+
+        define_method(:initialize) do |**opts|
+          attributes.each do |attr_name, attr_type|
+            value = opts.fetch(attr_name)
+
+            unless attr_type === value
+              raise TypeError, "Expected #{attr_name} to be of type #{attr_type}, got #{value.class}"
+            end
+
+            instance_variable_set(:"@#{attr_name}", value.freeze)
+          end
+        end
+
+        define_method(:hash) do
+          ordered_attribute_keys.map { |key| send(key) }.hash
+        end
+
+        define_method(:eql?) do |other|
+          return false unless self.class == other.class
+
+          ordered_attribute_keys.all? do |key|
+            send(key).eql?(other.send(key))
+          end
+        end
+
+        alias_method(:==, :eql?)
+
+        instance_eval(&blk) if blk
+      end
+    end
+  end
+
+  module TypedTaggedUnion
+    def self.new(**alternatives)
+      base =
+        Class.new do
+          define_singleton_method(:create) do |name, **attributes|
+            const_get(name.to_s.camelize.to_sym).new(**attributes)
+          end
+        end
+
+      alternatives.each do |tag, attributes|
+        alternative_klass =
+          TypedStruct.new(base, **attributes) do
+            define_singleton_method(:tag) do
+              tag
+            end
+          end
+
+        base.const_set(
+          tag.to_s.camelize.to_sym,
+          alternative_klass
+        )
+      end
+
+      base
+    end
+  end
+
+  class Boolean
+    def ===(other)
+      TrueClass === other || FalseClass === other
+    end
+  end
+
+  module OrNil
+    def self.[](klass)
+      Class.new do
+        define_singleton_method(:===) do |other|
+          other.nil? || klass === other
+        end
+      end
+    end
+  end
+end
diff --git a/plugin.rb b/plugin.rb
index 14547a5..f85e545 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -115,6 +115,7 @@ after_initialize do
   end
 
   require File.expand_path("../app/controllers/discourse_code_review/code_review_controller.rb", __FILE__)
+  require File.expand_path("../lib/typed_data.rb", __FILE__)
   require File.expand_path("../lib/discourse_code_review/github_user_syncer.rb", __FILE__)
   require File.expand_path("../lib/discourse_code_review/github_category_syncer.rb", __FILE__)
   require File.expand_path("../lib/discourse_code_review/importer.rb", __FILE__)

GitHub sha: 1bfeec60