kirei 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/README.md +40 -7
 - data/kirei.gemspec +1 -0
 - data/lib/cli/commands/new_app/execute.rb +1 -1
 - data/lib/cli/commands/new_app/files/app.rb +9 -2
 - data/lib/cli/commands/new_app/files/routes.rb +2 -2
 - data/lib/cli/commands/start.rb +1 -1
 - data/lib/kirei/app.rb +3 -0
 - data/lib/kirei/config.rb +4 -1
 - data/lib/kirei/errors/json_api_error.rb +25 -0
 - data/lib/kirei/errors/json_api_error_source.rb +12 -0
 - data/lib/kirei/logging/level.rb +33 -0
 - data/lib/kirei/logging/logger.rb +198 -0
 - data/lib/kirei/logging/metric.rb +40 -0
 - data/lib/kirei/model/base_class_interface.rb +55 -0
 - data/lib/kirei/model/class_methods.rb +157 -0
 - data/lib/kirei/model/human_id_generator.rb +40 -0
 - data/lib/kirei/model.rb +4 -175
 - data/lib/kirei/routing/base.rb +43 -12
 - data/lib/kirei/routing/route.rb +13 -0
 - data/lib/kirei/routing/router.rb +1 -31
 - data/lib/kirei/routing/verb.rb +37 -0
 - data/lib/kirei/services/result.rb +53 -0
 - data/lib/kirei/services/runner.rb +47 -0
 - data/lib/kirei/version.rb +1 -1
 - data/lib/kirei.rb +5 -2
 - data/sorbet/rbi/shims/ruby.rbi +15 -0
 - metadata +29 -3
 - data/lib/kirei/logger.rb +0 -196
 
| 
         @@ -0,0 +1,157 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # typed: strict
         
     | 
| 
      
 2 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Kirei
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 6 
     | 
    
         
            +
                module ClassMethods
         
     | 
| 
      
 7 
     | 
    
         
            +
                  extend T::Sig
         
     | 
| 
      
 8 
     | 
    
         
            +
                  extend T::Generic
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  # the attached class is the class that extends this module
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # e.g. "User", "Airport", ..
         
     | 
| 
      
 12 
     | 
    
         
            +
                  has_attached_class!
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  include Kirei::Model::BaseClassInterface
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  # defaults to a pluralized, underscored version of the class name
         
     | 
| 
      
 17 
     | 
    
         
            +
                  sig { override.returns(String) }
         
     | 
| 
      
 18 
     | 
    
         
            +
                  def table_name
         
     | 
| 
      
 19 
     | 
    
         
            +
                    @table_name ||= T.let("#{model_name}s", T.nilable(String))
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  sig { returns(String) }
         
     | 
| 
      
 23 
     | 
    
         
            +
                  def model_name
         
     | 
| 
      
 24 
     | 
    
         
            +
                    Kirei::Helpers.underscore(T.must(name.split("::").last))
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  sig { override.returns(Sequel::Dataset) }
         
     | 
| 
      
 28 
     | 
    
         
            +
                  def query
         
     | 
| 
      
 29 
     | 
    
         
            +
                    db[table_name.to_sym]
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  sig { override.returns(Sequel::Database) }
         
     | 
| 
      
 33 
     | 
    
         
            +
                  def db
         
     | 
| 
      
 34 
     | 
    
         
            +
                    App.raw_db_connection
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  sig do
         
     | 
| 
      
 38 
     | 
    
         
            +
                    override.params(
         
     | 
| 
      
 39 
     | 
    
         
            +
                      hash: T::Hash[Symbol, T.untyped],
         
     | 
| 
      
 40 
     | 
    
         
            +
                    ).returns(T::Array[T.attached_class])
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  def where(hash)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    resolve(query.where(hash))
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  sig { override.returns(T::Array[T.attached_class]) }
         
     | 
| 
      
 47 
     | 
    
         
            +
                  def all
         
     | 
| 
      
 48 
     | 
    
         
            +
                    resolve(query.all)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
         
     | 
| 
      
 52 
     | 
    
         
            +
                  # default values defined in the model are used, if omitted in the hash
         
     | 
| 
      
 53 
     | 
    
         
            +
                  sig do
         
     | 
| 
      
 54 
     | 
    
         
            +
                    override.params(
         
     | 
| 
      
 55 
     | 
    
         
            +
                      hash: T::Hash[Symbol, T.untyped],
         
     | 
| 
      
 56 
     | 
    
         
            +
                    ).returns(T.attached_class)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
                  def create(hash)
         
     | 
| 
      
 59 
     | 
    
         
            +
                    # instantiate a new object to ensure we use default values defined in the model
         
     | 
| 
      
 60 
     | 
    
         
            +
                    without_id = !hash.key?(:id)
         
     | 
| 
      
 61 
     | 
    
         
            +
                    hash[:id] = "kirei-fake-id" if without_id
         
     | 
| 
      
 62 
     | 
    
         
            +
                    new_record = from_hash(Helpers.deep_stringify_keys(hash))
         
     | 
| 
      
 63 
     | 
    
         
            +
                    all_attributes = T.let(new_record.serialize, T::Hash[String, T.untyped])
         
     | 
| 
      
 64 
     | 
    
         
            +
                    all_attributes.delete("id") if without_id && all_attributes["id"] == "kirei-fake-id"
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                    wrap_jsonb_non_primivitives!(all_attributes)
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                    if new_record.respond_to?(:created_at) && all_attributes["created_at"].nil?
         
     | 
| 
      
 69 
     | 
    
         
            +
                      all_attributes["created_at"] = Time.now.utc
         
     | 
| 
      
 70 
     | 
    
         
            +
                    end
         
     | 
| 
      
 71 
     | 
    
         
            +
                    if new_record.respond_to?(:updated_at) && all_attributes["updated_at"].nil?
         
     | 
| 
      
 72 
     | 
    
         
            +
                      all_attributes["updated_at"] = Time.now.utc
         
     | 
| 
      
 73 
     | 
    
         
            +
                    end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                    pkey = T.let(query.insert(all_attributes), String)
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                    T.must(find_by({ id: pkey }))
         
     | 
| 
      
 78 
     | 
    
         
            +
                  end
         
     | 
| 
      
 79 
     | 
    
         
            +
                  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                  sig { override.params(attributes: T::Hash[T.any(Symbol, String), T.untyped]).void }
         
     | 
| 
      
 82 
     | 
    
         
            +
                  def wrap_jsonb_non_primivitives!(attributes)
         
     | 
| 
      
 83 
     | 
    
         
            +
                    # setting `@raw_db_connection.wrap_json_primitives = true`
         
     | 
| 
      
 84 
     | 
    
         
            +
                    # only works on JSON primitives, but not on blank hashes/arrays
         
     | 
| 
      
 85 
     | 
    
         
            +
                    return unless App.config.db_extensions.include?(:pg_json)
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                    attributes.each_pair do |key, value|
         
     | 
| 
      
 88 
     | 
    
         
            +
                      next unless value.is_a?(Hash) || value.is_a?(Array)
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                      attributes[key] = T.unsafe(Sequel).pg_jsonb_wrap(value)
         
     | 
| 
      
 91 
     | 
    
         
            +
                    end
         
     | 
| 
      
 92 
     | 
    
         
            +
                  end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                  sig do
         
     | 
| 
      
 95 
     | 
    
         
            +
                    override.params(
         
     | 
| 
      
 96 
     | 
    
         
            +
                      hash: T::Hash[Symbol, T.untyped],
         
     | 
| 
      
 97 
     | 
    
         
            +
                    ).returns(T.nilable(T.attached_class))
         
     | 
| 
      
 98 
     | 
    
         
            +
                  end
         
     | 
| 
      
 99 
     | 
    
         
            +
                  def find_by(hash)
         
     | 
| 
      
 100 
     | 
    
         
            +
                    resolve_first(query.where(hash))
         
     | 
| 
      
 101 
     | 
    
         
            +
                  end
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                  # Extra or unknown properties present in the Hash do not raise exceptions at
         
     | 
| 
      
 104 
     | 
    
         
            +
                  # runtime unless the optional strict argument to from_hash is passed
         
     | 
| 
      
 105 
     | 
    
         
            +
                  #
         
     | 
| 
      
 106 
     | 
    
         
            +
                  # Source: https://sorbet.org/docs/tstruct#from_hash-gotchas
         
     | 
| 
      
 107 
     | 
    
         
            +
                  # "strict" defaults to "false".
         
     | 
| 
      
 108 
     | 
    
         
            +
                  sig do
         
     | 
| 
      
 109 
     | 
    
         
            +
                    override.params(
         
     | 
| 
      
 110 
     | 
    
         
            +
                      query: T.any(Sequel::Dataset, T::Array[T::Hash[Symbol, T.untyped]]),
         
     | 
| 
      
 111 
     | 
    
         
            +
                      strict: T.nilable(T::Boolean),
         
     | 
| 
      
 112 
     | 
    
         
            +
                    ).returns(T::Array[T.attached_class])
         
     | 
| 
      
 113 
     | 
    
         
            +
                  end
         
     | 
| 
      
 114 
     | 
    
         
            +
                  def resolve(query, strict = nil)
         
     | 
| 
      
 115 
     | 
    
         
            +
                    strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                    query.map do |row|
         
     | 
| 
      
 118 
     | 
    
         
            +
                      row = T.cast(row, T::Hash[Symbol, T.untyped])
         
     | 
| 
      
 119 
     | 
    
         
            +
                      row.transform_keys!(&:to_s) # sequel returns symbolized keys
         
     | 
| 
      
 120 
     | 
    
         
            +
                      from_hash(row, strict_loading)
         
     | 
| 
      
 121 
     | 
    
         
            +
                    end
         
     | 
| 
      
 122 
     | 
    
         
            +
                  end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                  sig do
         
     | 
| 
      
 125 
     | 
    
         
            +
                    override.params(
         
     | 
| 
      
 126 
     | 
    
         
            +
                      query: Sequel::Dataset,
         
     | 
| 
      
 127 
     | 
    
         
            +
                      strict: T.nilable(T::Boolean),
         
     | 
| 
      
 128 
     | 
    
         
            +
                    ).returns(T.nilable(T.attached_class))
         
     | 
| 
      
 129 
     | 
    
         
            +
                  end
         
     | 
| 
      
 130 
     | 
    
         
            +
                  def resolve_first(query, strict = nil)
         
     | 
| 
      
 131 
     | 
    
         
            +
                    strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                    resolve(query.limit(1), strict_loading).first
         
     | 
| 
      
 134 
     | 
    
         
            +
                  end
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
                  # defaults to 6
         
     | 
| 
      
 137 
     | 
    
         
            +
                  sig { override.returns(Integer) }
         
     | 
| 
      
 138 
     | 
    
         
            +
                  def human_id_length = 6
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                  # defaults to "model_name" (table_name without the trailing "s")
         
     | 
| 
      
 141 
     | 
    
         
            +
                  sig { override.returns(String) }
         
     | 
| 
      
 142 
     | 
    
         
            +
                  def human_id_prefix = model_name
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                  #
         
     | 
| 
      
 145 
     | 
    
         
            +
                  # Generates a human-readable ID for the record.
         
     | 
| 
      
 146 
     | 
    
         
            +
                  # The ID is prefixed with the table name and an underscore.
         
     | 
| 
      
 147 
     | 
    
         
            +
                  #
         
     | 
| 
      
 148 
     | 
    
         
            +
                  sig { override.returns(String) }
         
     | 
| 
      
 149 
     | 
    
         
            +
                  def generate_human_id
         
     | 
| 
      
 150 
     | 
    
         
            +
                    Kirei::Model::HumanIdGenerator.call(
         
     | 
| 
      
 151 
     | 
    
         
            +
                      length: human_id_length,
         
     | 
| 
      
 152 
     | 
    
         
            +
                      prefix: human_id_prefix,
         
     | 
| 
      
 153 
     | 
    
         
            +
                    )
         
     | 
| 
      
 154 
     | 
    
         
            +
                  end
         
     | 
| 
      
 155 
     | 
    
         
            +
                end
         
     | 
| 
      
 156 
     | 
    
         
            +
              end
         
     | 
| 
      
 157 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,40 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # typed: strict
         
     | 
| 
      
 2 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Kirei
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 6 
     | 
    
         
            +
                class HumanIdGenerator
         
     | 
| 
      
 7 
     | 
    
         
            +
                  extend T::Sig
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  # Removed ambiguous characters 0, 1, O, I, l, 5, S
         
     | 
| 
      
 10 
     | 
    
         
            +
                  ALLOWED_CHARS = "2346789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrtuvwxyz"
         
     | 
| 
      
 11 
     | 
    
         
            +
                  private_constant :ALLOWED_CHARS
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  ALLOWED_CHARS_COUNT = T.let(ALLOWED_CHARS.size, Integer)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  private_constant :ALLOWED_CHARS_COUNT
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  sig do
         
     | 
| 
      
 17 
     | 
    
         
            +
                    params(
         
     | 
| 
      
 18 
     | 
    
         
            +
                      length: Integer,
         
     | 
| 
      
 19 
     | 
    
         
            +
                      prefix: String,
         
     | 
| 
      
 20 
     | 
    
         
            +
                    ).returns(String)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
                  def self.call(length:, prefix:)
         
     | 
| 
      
 23 
     | 
    
         
            +
                    "#{prefix}_#{random_id(length)}"
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  sig { params(key_length: Integer).returns(String) }
         
     | 
| 
      
 27 
     | 
    
         
            +
                  private_class_method def self.random_id(key_length)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    random_chars = Array.new(key_length)
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    key_length.times do |idx|
         
     | 
| 
      
 31 
     | 
    
         
            +
                      random_char_idx = SecureRandom.random_number(ALLOWED_CHARS_COUNT)
         
     | 
| 
      
 32 
     | 
    
         
            +
                      random_char = T.must(ALLOWED_CHARS[random_char_idx])
         
     | 
| 
      
 33 
     | 
    
         
            +
                      random_chars[idx] = random_char
         
     | 
| 
      
 34 
     | 
    
         
            +
                    end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                    random_chars.join
         
     | 
| 
      
 37 
     | 
    
         
            +
                  end
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/kirei/model.rb
    CHANGED
    
    | 
         @@ -6,7 +6,7 @@ module Kirei 
     | 
|
| 
       6 
6 
     | 
    
         
             
                extend T::Sig
         
     | 
| 
       7 
7 
     | 
    
         
             
                extend T::Helpers
         
     | 
| 
       8 
8 
     | 
    
         | 
| 
       9 
     | 
    
         
            -
                sig { returns(BaseClassInterface) }
         
     | 
| 
      
 9 
     | 
    
         
            +
                sig { returns(Kirei::Model::BaseClassInterface) }
         
     | 
| 
       10 
10 
     | 
    
         
             
                def class; super; end # rubocop:disable all
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
12 
     | 
    
         
             
                # An update keeps the original object intact, and returns a new object with the updated values.
         
     | 
| 
         @@ -18,7 +18,7 @@ module Kirei 
     | 
|
| 
       18 
18 
     | 
    
         
             
                def update(hash)
         
     | 
| 
       19 
19 
     | 
    
         
             
                  hash[:updated_at] = Time.now.utc if respond_to?(:updated_at) && hash[:updated_at].nil?
         
     | 
| 
       20 
20 
     | 
    
         
             
                  self.class.wrap_jsonb_non_primivitives!(hash)
         
     | 
| 
       21 
     | 
    
         
            -
                  self.class. 
     | 
| 
      
 21 
     | 
    
         
            +
                  self.class.query.where({ id: id }).update(hash)
         
     | 
| 
       22 
22 
     | 
    
         
             
                  self.class.find_by({ id: id })
         
     | 
| 
       23 
23 
     | 
    
         
             
                end
         
     | 
| 
       24 
24 
     | 
    
         | 
| 
         @@ -26,7 +26,7 @@ module Kirei 
     | 
|
| 
       26 
26 
     | 
    
         
             
                # Calling delete multiple times will return false after the first (successful) call.
         
     | 
| 
       27 
27 
     | 
    
         
             
                sig { returns(T::Boolean) }
         
     | 
| 
       28 
28 
     | 
    
         
             
                def delete
         
     | 
| 
       29 
     | 
    
         
            -
                  count = self.class. 
     | 
| 
      
 29 
     | 
    
         
            +
                  count = self.class.query.where({ id: id }).delete
         
     | 
| 
       30 
30 
     | 
    
         
             
                  count == 1
         
     | 
| 
       31 
31 
     | 
    
         
             
                end
         
     | 
| 
       32 
32 
     | 
    
         | 
| 
         @@ -47,177 +47,6 @@ module Kirei 
     | 
|
| 
       47 
47 
     | 
    
         
             
                  end
         
     | 
| 
       48 
48 
     | 
    
         
             
                end
         
     | 
| 
       49 
49 
     | 
    
         | 
| 
       50 
     | 
    
         
            -
                 
     | 
| 
       51 
     | 
    
         
            -
                  extend T::Sig
         
     | 
| 
       52 
     | 
    
         
            -
                  extend T::Helpers
         
     | 
| 
       53 
     | 
    
         
            -
                  interface!
         
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
     | 
    
         
            -
                  sig { abstract.params(hash: T.untyped).returns(T.untyped) }
         
     | 
| 
       56 
     | 
    
         
            -
                  def find_by(hash)
         
     | 
| 
       57 
     | 
    
         
            -
                  end
         
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
       59 
     | 
    
         
            -
                  sig { abstract.params(hash: T.untyped).returns(T.untyped) }
         
     | 
| 
       60 
     | 
    
         
            -
                  def where(hash)
         
     | 
| 
       61 
     | 
    
         
            -
                  end
         
     | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
       63 
     | 
    
         
            -
                  sig { abstract.returns(T.untyped) }
         
     | 
| 
       64 
     | 
    
         
            -
                  def all
         
     | 
| 
       65 
     | 
    
         
            -
                  end
         
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
       67 
     | 
    
         
            -
                  sig { abstract.params(hash: T.untyped).returns(T.untyped) }
         
     | 
| 
       68 
     | 
    
         
            -
                  def create(hash)
         
     | 
| 
       69 
     | 
    
         
            -
                  end
         
     | 
| 
       70 
     | 
    
         
            -
             
     | 
| 
       71 
     | 
    
         
            -
                  sig { abstract.params(attributes: T.untyped).void }
         
     | 
| 
       72 
     | 
    
         
            -
                  def wrap_jsonb_non_primivitives!(attributes)
         
     | 
| 
       73 
     | 
    
         
            -
                  end
         
     | 
| 
       74 
     | 
    
         
            -
             
     | 
| 
       75 
     | 
    
         
            -
                  sig { abstract.params(hash: T.untyped).returns(T.untyped) }
         
     | 
| 
       76 
     | 
    
         
            -
                  def resolve(hash)
         
     | 
| 
       77 
     | 
    
         
            -
                  end
         
     | 
| 
       78 
     | 
    
         
            -
             
     | 
| 
       79 
     | 
    
         
            -
                  sig { abstract.params(hash: T.untyped).returns(T.untyped) }
         
     | 
| 
       80 
     | 
    
         
            -
                  def resolve_first(hash)
         
     | 
| 
       81 
     | 
    
         
            -
                  end
         
     | 
| 
       82 
     | 
    
         
            -
             
     | 
| 
       83 
     | 
    
         
            -
                  sig { abstract.returns(T.untyped) }
         
     | 
| 
       84 
     | 
    
         
            -
                  def table_name
         
     | 
| 
       85 
     | 
    
         
            -
                  end
         
     | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
       87 
     | 
    
         
            -
                  sig { abstract.returns(T.untyped) }
         
     | 
| 
       88 
     | 
    
         
            -
                  def db
         
     | 
| 
       89 
     | 
    
         
            -
                  end
         
     | 
| 
       90 
     | 
    
         
            -
                end
         
     | 
| 
       91 
     | 
    
         
            -
             
     | 
| 
       92 
     | 
    
         
            -
                module ClassMethods
         
     | 
| 
       93 
     | 
    
         
            -
                  extend T::Sig
         
     | 
| 
       94 
     | 
    
         
            -
                  extend T::Generic
         
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
                  # the attached class is the class that extends this module
         
     | 
| 
       97 
     | 
    
         
            -
                  # e.g. "User"
         
     | 
| 
       98 
     | 
    
         
            -
                  # extend T::Generic
         
     | 
| 
       99 
     | 
    
         
            -
                  # has_attached_class!
         
     | 
| 
       100 
     | 
    
         
            -
                  has_attached_class!
         
     | 
| 
       101 
     | 
    
         
            -
             
     | 
| 
       102 
     | 
    
         
            -
                  include BaseClassInterface
         
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
       104 
     | 
    
         
            -
                  # defaults to a pluralized, underscored version of the class name
         
     | 
| 
       105 
     | 
    
         
            -
                  sig { override.returns(String) }
         
     | 
| 
       106 
     | 
    
         
            -
                  def table_name
         
     | 
| 
       107 
     | 
    
         
            -
                    @table_name ||= T.let(
         
     | 
| 
       108 
     | 
    
         
            -
                      begin
         
     | 
| 
       109 
     | 
    
         
            -
                        table_name_ = Kirei::Helpers.underscore(T.must(name.split("::").last))
         
     | 
| 
       110 
     | 
    
         
            -
                        "#{table_name_}s"
         
     | 
| 
       111 
     | 
    
         
            -
                      end,
         
     | 
| 
       112 
     | 
    
         
            -
                      T.nilable(String),
         
     | 
| 
       113 
     | 
    
         
            -
                    )
         
     | 
| 
       114 
     | 
    
         
            -
                  end
         
     | 
| 
       115 
     | 
    
         
            -
             
     | 
| 
       116 
     | 
    
         
            -
                  sig { override.returns(Sequel::Dataset) }
         
     | 
| 
       117 
     | 
    
         
            -
                  def db
         
     | 
| 
       118 
     | 
    
         
            -
                    App.raw_db_connection[table_name.to_sym]
         
     | 
| 
       119 
     | 
    
         
            -
                  end
         
     | 
| 
       120 
     | 
    
         
            -
             
     | 
| 
       121 
     | 
    
         
            -
                  sig do
         
     | 
| 
       122 
     | 
    
         
            -
                    override.params(
         
     | 
| 
       123 
     | 
    
         
            -
                      hash: T::Hash[Symbol, T.untyped],
         
     | 
| 
       124 
     | 
    
         
            -
                    ).returns(T::Array[T.attached_class])
         
     | 
| 
       125 
     | 
    
         
            -
                  end
         
     | 
| 
       126 
     | 
    
         
            -
                  def where(hash)
         
     | 
| 
       127 
     | 
    
         
            -
                    resolve(db.where(hash))
         
     | 
| 
       128 
     | 
    
         
            -
                  end
         
     | 
| 
       129 
     | 
    
         
            -
             
     | 
| 
       130 
     | 
    
         
            -
                  sig { override.returns(T::Array[T.attached_class]) }
         
     | 
| 
       131 
     | 
    
         
            -
                  def all
         
     | 
| 
       132 
     | 
    
         
            -
                    resolve(db.all)
         
     | 
| 
       133 
     | 
    
         
            -
                  end
         
     | 
| 
       134 
     | 
    
         
            -
             
     | 
| 
       135 
     | 
    
         
            -
                  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
         
     | 
| 
       136 
     | 
    
         
            -
                  # default values defined in the model are used, if omitted in the hash
         
     | 
| 
       137 
     | 
    
         
            -
                  sig do
         
     | 
| 
       138 
     | 
    
         
            -
                    override.params(
         
     | 
| 
       139 
     | 
    
         
            -
                      hash: T::Hash[Symbol, T.untyped],
         
     | 
| 
       140 
     | 
    
         
            -
                    ).returns(T.attached_class)
         
     | 
| 
       141 
     | 
    
         
            -
                  end
         
     | 
| 
       142 
     | 
    
         
            -
                  def create(hash)
         
     | 
| 
       143 
     | 
    
         
            -
                    # instantiate a new object to ensure we use default values defined in the model
         
     | 
| 
       144 
     | 
    
         
            -
                    without_id = !hash.key?(:id)
         
     | 
| 
       145 
     | 
    
         
            -
                    hash[:id] = "kirei-fake-id" if without_id
         
     | 
| 
       146 
     | 
    
         
            -
                    new_record = from_hash(Helpers.deep_stringify_keys(hash))
         
     | 
| 
       147 
     | 
    
         
            -
                    all_attributes = T.let(new_record.serialize, T::Hash[String, T.untyped])
         
     | 
| 
       148 
     | 
    
         
            -
                    all_attributes.delete("id") if without_id && all_attributes["id"] == "kirei-fake-id"
         
     | 
| 
       149 
     | 
    
         
            -
             
     | 
| 
       150 
     | 
    
         
            -
                    wrap_jsonb_non_primivitives!(all_attributes)
         
     | 
| 
       151 
     | 
    
         
            -
             
     | 
| 
       152 
     | 
    
         
            -
                    if new_record.respond_to?(:created_at) && all_attributes["created_at"].nil?
         
     | 
| 
       153 
     | 
    
         
            -
                      all_attributes["created_at"] = Time.now.utc
         
     | 
| 
       154 
     | 
    
         
            -
                    end
         
     | 
| 
       155 
     | 
    
         
            -
                    if new_record.respond_to?(:updated_at) && all_attributes["updated_at"].nil?
         
     | 
| 
       156 
     | 
    
         
            -
                      all_attributes["updated_at"] = Time.now.utc
         
     | 
| 
       157 
     | 
    
         
            -
                    end
         
     | 
| 
       158 
     | 
    
         
            -
             
     | 
| 
       159 
     | 
    
         
            -
                    pkey = T.let(db.insert(all_attributes), String)
         
     | 
| 
       160 
     | 
    
         
            -
             
     | 
| 
       161 
     | 
    
         
            -
                    T.must(find_by({ id: pkey }))
         
     | 
| 
       162 
     | 
    
         
            -
                  end
         
     | 
| 
       163 
     | 
    
         
            -
                  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
         
     | 
| 
       164 
     | 
    
         
            -
             
     | 
| 
       165 
     | 
    
         
            -
                  sig { override.params(attributes: T::Hash[T.any(Symbol, String), T.untyped]).void }
         
     | 
| 
       166 
     | 
    
         
            -
                  def wrap_jsonb_non_primivitives!(attributes)
         
     | 
| 
       167 
     | 
    
         
            -
                    # setting `@raw_db_connection.wrap_json_primitives = true`
         
     | 
| 
       168 
     | 
    
         
            -
                    # only works on JSON primitives, but not on blank hashes/arrays
         
     | 
| 
       169 
     | 
    
         
            -
                    return unless App.config.db_extensions.include?(:pg_json)
         
     | 
| 
       170 
     | 
    
         
            -
             
     | 
| 
       171 
     | 
    
         
            -
                    attributes.each_pair do |key, value|
         
     | 
| 
       172 
     | 
    
         
            -
                      next unless value.is_a?(Hash) || value.is_a?(Array)
         
     | 
| 
       173 
     | 
    
         
            -
             
     | 
| 
       174 
     | 
    
         
            -
                      attributes[key] = T.unsafe(Sequel).pg_jsonb_wrap(value)
         
     | 
| 
       175 
     | 
    
         
            -
                    end
         
     | 
| 
       176 
     | 
    
         
            -
                  end
         
     | 
| 
       177 
     | 
    
         
            -
             
     | 
| 
       178 
     | 
    
         
            -
                  sig do
         
     | 
| 
       179 
     | 
    
         
            -
                    override.params(
         
     | 
| 
       180 
     | 
    
         
            -
                      hash: T::Hash[Symbol, T.untyped],
         
     | 
| 
       181 
     | 
    
         
            -
                    ).returns(T.nilable(T.attached_class))
         
     | 
| 
       182 
     | 
    
         
            -
                  end
         
     | 
| 
       183 
     | 
    
         
            -
                  def find_by(hash)
         
     | 
| 
       184 
     | 
    
         
            -
                    resolve_first(db.where(hash))
         
     | 
| 
       185 
     | 
    
         
            -
                  end
         
     | 
| 
       186 
     | 
    
         
            -
             
     | 
| 
       187 
     | 
    
         
            -
                  # Extra or unknown properties present in the Hash do not raise exceptions at
         
     | 
| 
       188 
     | 
    
         
            -
                  # runtime unless the optional strict argument to from_hash is passed
         
     | 
| 
       189 
     | 
    
         
            -
                  #
         
     | 
| 
       190 
     | 
    
         
            -
                  # Source: https://sorbet.org/docs/tstruct#from_hash-gotchas
         
     | 
| 
       191 
     | 
    
         
            -
                  # "strict" defaults to "false".
         
     | 
| 
       192 
     | 
    
         
            -
                  sig do
         
     | 
| 
       193 
     | 
    
         
            -
                    override.params(
         
     | 
| 
       194 
     | 
    
         
            -
                      query: T.any(Sequel::Dataset, T::Array[T::Hash[Symbol, T.untyped]]),
         
     | 
| 
       195 
     | 
    
         
            -
                      strict: T.nilable(T::Boolean),
         
     | 
| 
       196 
     | 
    
         
            -
                    ).returns(T::Array[T.attached_class])
         
     | 
| 
       197 
     | 
    
         
            -
                  end
         
     | 
| 
       198 
     | 
    
         
            -
                  def resolve(query, strict = nil)
         
     | 
| 
       199 
     | 
    
         
            -
                    strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
         
     | 
| 
       200 
     | 
    
         
            -
             
     | 
| 
       201 
     | 
    
         
            -
                    query.map do |row|
         
     | 
| 
       202 
     | 
    
         
            -
                      row = T.cast(row, T::Hash[Symbol, T.untyped])
         
     | 
| 
       203 
     | 
    
         
            -
                      row.transform_keys!(&:to_s) # sequel returns symbolized keys
         
     | 
| 
       204 
     | 
    
         
            -
                      from_hash(row, strict_loading)
         
     | 
| 
       205 
     | 
    
         
            -
                    end
         
     | 
| 
       206 
     | 
    
         
            -
                  end
         
     | 
| 
       207 
     | 
    
         
            -
             
     | 
| 
       208 
     | 
    
         
            -
                  sig do
         
     | 
| 
       209 
     | 
    
         
            -
                    override.params(
         
     | 
| 
       210 
     | 
    
         
            -
                      query: Sequel::Dataset,
         
     | 
| 
       211 
     | 
    
         
            -
                      strict: T.nilable(T::Boolean),
         
     | 
| 
       212 
     | 
    
         
            -
                    ).returns(T.nilable(T.attached_class))
         
     | 
| 
       213 
     | 
    
         
            -
                  end
         
     | 
| 
       214 
     | 
    
         
            -
                  def resolve_first(query, strict = nil)
         
     | 
| 
       215 
     | 
    
         
            -
                    strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
         
     | 
| 
       216 
     | 
    
         
            -
             
     | 
| 
       217 
     | 
    
         
            -
                    resolve(query.limit(1), strict_loading).first
         
     | 
| 
       218 
     | 
    
         
            -
                  end
         
     | 
| 
       219 
     | 
    
         
            -
                end
         
     | 
| 
       220 
     | 
    
         
            -
             
     | 
| 
       221 
     | 
    
         
            -
                mixes_in_class_methods(ClassMethods)
         
     | 
| 
      
 50 
     | 
    
         
            +
                mixes_in_class_methods(Kirei::Model::ClassMethods)
         
     | 
| 
       222 
51 
     | 
    
         
             
              end
         
     | 
| 
       223 
52 
     | 
    
         
             
            end
         
     | 
    
        data/lib/kirei/routing/base.rb
    CHANGED
    
    | 
         @@ -17,9 +17,14 @@ module Kirei 
     | 
|
| 
       17 
17 
     | 
    
         
             
                  sig { returns(T::Hash[String, T.untyped]) }
         
     | 
| 
       18 
18 
     | 
    
         
             
                  attr_reader :params
         
     | 
| 
       19 
19 
     | 
    
         | 
| 
      
 20 
     | 
    
         
            +
                  sig { returns(Router) }
         
     | 
| 
      
 21 
     | 
    
         
            +
                  attr_reader :router; private :router
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
       20 
23 
     | 
    
         
             
                  sig { params(env: RackEnvType).returns(RackResponseType) }
         
     | 
| 
       21 
24 
     | 
    
         
             
                  def call(env)
         
     | 
| 
       22 
     | 
    
         
            -
                     
     | 
| 
      
 25 
     | 
    
         
            +
                    start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                    http_verb = Verb.deserialize(env.fetch("REQUEST_METHOD"))
         
     | 
| 
       23 
28 
     | 
    
         
             
                    req_path = T.cast(env.fetch("REQUEST_PATH"), String)
         
     | 
| 
       24 
29 
     | 
    
         
             
                    #
         
     | 
| 
       25 
30 
     | 
    
         
             
                    # TODO: reject requests from unexpected hosts -> allow configuring allowed hosts in a `cors.rb` file
         
     | 
| 
         @@ -27,18 +32,18 @@ module Kirei 
     | 
|
| 
       27 
32 
     | 
    
         
             
                    # -> use https://github.com/cyu/rack-cors ?
         
     | 
| 
       28 
33 
     | 
    
         
             
                    #
         
     | 
| 
       29 
34 
     | 
    
         | 
| 
       30 
     | 
    
         
            -
                    route =  
     | 
| 
      
 35 
     | 
    
         
            +
                    route = router.get(http_verb, req_path)
         
     | 
| 
       31 
36 
     | 
    
         
             
                    return [404, {}, ["Not Found"]] if route.nil?
         
     | 
| 
       32 
37 
     | 
    
         | 
| 
       33 
38 
     | 
    
         
             
                    params = case route.verb
         
     | 
| 
       34 
     | 
    
         
            -
                             when  
     | 
| 
      
 39 
     | 
    
         
            +
                             when Verb::GET
         
     | 
| 
       35 
40 
     | 
    
         
             
                               query = T.cast(env.fetch("QUERY_STRING"), String)
         
     | 
| 
       36 
41 
     | 
    
         
             
                               query.split("&").to_h do |p|
         
     | 
| 
       37 
42 
     | 
    
         
             
                                 k, v = p.split("=")
         
     | 
| 
       38 
43 
     | 
    
         
             
                                 k = T.cast(k, String)
         
     | 
| 
       39 
44 
     | 
    
         
             
                                 [k, v]
         
     | 
| 
       40 
45 
     | 
    
         
             
                               end
         
     | 
| 
       41 
     | 
    
         
            -
                             when  
     | 
| 
      
 46 
     | 
    
         
            +
                             when Verb::POST, Verb::PUT, Verb::PATCH
         
     | 
| 
       42 
47 
     | 
    
         
             
                               # TODO: based on content-type, parse the body differently
         
     | 
| 
       43 
48 
     | 
    
         
             
                               #       build-in support for JSON & XML
         
     | 
| 
       44 
49 
     | 
    
         
             
                               body = T.cast(env.fetch("rack.input"), T.any(IO, StringIO))
         
     | 
| 
         @@ -46,7 +51,7 @@ module Kirei 
     | 
|
| 
       46 
51 
     | 
    
         
             
                               body.rewind # TODO: maybe don't rewind if we don't need to?
         
     | 
| 
       47 
52 
     | 
    
         
             
                               T.cast(res, T::Hash[String, T.untyped])
         
     | 
| 
       48 
53 
     | 
    
         
             
                             else
         
     | 
| 
       49 
     | 
    
         
            -
                               Logger.logger.warn("Unsupported HTTP verb: #{http_verb.serialize} send to #{req_path}")
         
     | 
| 
      
 54 
     | 
    
         
            +
                               Logging::Logger.logger.warn("Unsupported HTTP verb: #{http_verb.serialize} send to #{req_path}")
         
     | 
| 
       50 
55 
     | 
    
         
             
                               {}
         
     | 
| 
       51 
56 
     | 
    
         
             
                    end
         
     | 
| 
       52 
57 
     | 
    
         | 
| 
         @@ -58,7 +63,19 @@ module Kirei 
     | 
|
| 
       58 
63 
     | 
    
         
             
                    before_hooks = collect_hooks(controller, :before_hooks)
         
     | 
| 
       59 
64 
     | 
    
         
             
                    run_hooks(before_hooks)
         
     | 
| 
       60 
65 
     | 
    
         | 
| 
       61 
     | 
    
         
            -
                     
     | 
| 
      
 66 
     | 
    
         
            +
                    Kirei::Logging::Logger.call(
         
     | 
| 
      
 67 
     | 
    
         
            +
                      level: Kirei::Logging::Level::INFO,
         
     | 
| 
      
 68 
     | 
    
         
            +
                      label: "Request Started",
         
     | 
| 
      
 69 
     | 
    
         
            +
                      meta: params,
         
     | 
| 
      
 70 
     | 
    
         
            +
                    )
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                    statsd_timing_tags = {
         
     | 
| 
      
 73 
     | 
    
         
            +
                      "controller" => controller.name,
         
     | 
| 
      
 74 
     | 
    
         
            +
                      "route" => route.action,
         
     | 
| 
      
 75 
     | 
    
         
            +
                    }
         
     | 
| 
      
 76 
     | 
    
         
            +
                    Logging::Metric.inject_defaults(statsd_timing_tags)
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                    status, headers, response_body = T.cast(
         
     | 
| 
       62 
79 
     | 
    
         
             
                      controller.new(params: params).public_send(route.action),
         
     | 
| 
       63 
80 
     | 
    
         
             
                      RackResponseType,
         
     | 
| 
       64 
81 
     | 
    
         
             
                    )
         
     | 
| 
         @@ -75,14 +92,30 @@ module Kirei 
     | 
|
| 
       75 
92 
     | 
    
         
             
                    [
         
     | 
| 
       76 
93 
     | 
    
         
             
                      status,
         
     | 
| 
       77 
94 
     | 
    
         
             
                      headers,
         
     | 
| 
       78 
     | 
    
         
            -
                       
     | 
| 
      
 95 
     | 
    
         
            +
                      response_body,
         
     | 
| 
       79 
96 
     | 
    
         
             
                    ]
         
     | 
| 
      
 97 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 98 
     | 
    
         
            +
                    stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
         
     | 
| 
      
 99 
     | 
    
         
            +
                    if start # early return for 404
         
     | 
| 
      
 100 
     | 
    
         
            +
                      latency_in_ms = stop - start
         
     | 
| 
      
 101 
     | 
    
         
            +
                      ::StatsD.measure("request", latency_in_ms, tags: statsd_timing_tags)
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                      Kirei::Logging::Logger.call(
         
     | 
| 
      
 104 
     | 
    
         
            +
                        level: Kirei::Logging::Level::INFO,
         
     | 
| 
      
 105 
     | 
    
         
            +
                        label: "Request Finished",
         
     | 
| 
      
 106 
     | 
    
         
            +
                        meta: { "response.body" => response_body, "response.latency_in_ms" => latency_in_ms },
         
     | 
| 
      
 107 
     | 
    
         
            +
                      )
         
     | 
| 
      
 108 
     | 
    
         
            +
                    end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                    # reset global variables after the request has been served
         
     | 
| 
      
 111 
     | 
    
         
            +
                    # and after all "after" hooks have run to avoid leaking
         
     | 
| 
      
 112 
     | 
    
         
            +
                    Thread.current[:enduser_id] = nil
         
     | 
| 
      
 113 
     | 
    
         
            +
                    Thread.current[:request_id] = nil
         
     | 
| 
       80 
114 
     | 
    
         
             
                  end
         
     | 
| 
       81 
115 
     | 
    
         | 
| 
       82 
116 
     | 
    
         
             
                  #
         
     | 
| 
       83 
     | 
    
         
            -
                  # Kirei::App#render
         
     | 
| 
       84 
117 
     | 
    
         
             
                  # * "status": defaults to 200
         
     | 
| 
       85 
     | 
    
         
            -
                  # * "headers":  
     | 
| 
      
 118 
     | 
    
         
            +
                  # * "headers": Kirei adds some default headers for security, but the user can override them
         
     | 
| 
       86 
119 
     | 
    
         
             
                  #
         
     | 
| 
       87 
120 
     | 
    
         
             
                  sig do
         
     | 
| 
       88 
121 
     | 
    
         
             
                    params(
         
     | 
| 
         @@ -92,8 +125,6 @@ module Kirei 
     | 
|
| 
       92 
125 
     | 
    
         
             
                    ).returns(RackResponseType)
         
     | 
| 
       93 
126 
     | 
    
         
             
                  end
         
     | 
| 
       94 
127 
     | 
    
         
             
                  def render(body, status: 200, headers: {})
         
     | 
| 
       95 
     | 
    
         
            -
                    # merge default headers
         
     | 
| 
       96 
     | 
    
         
            -
                    # support a "type" to set content-type header? (or default to json, and users must set the header themselves for other types?)
         
     | 
| 
       97 
128 
     | 
    
         
             
                    [
         
     | 
| 
       98 
129 
     | 
    
         
             
                      status,
         
     | 
| 
       99 
130 
     | 
    
         
             
                      headers,
         
     | 
| 
         @@ -103,7 +134,7 @@ module Kirei 
     | 
|
| 
       103 
134 
     | 
    
         | 
| 
       104 
135 
     | 
    
         
             
                  sig { returns(T::Hash[String, String]) }
         
     | 
| 
       105 
136 
     | 
    
         
             
                  def default_headers
         
     | 
| 
       106 
     | 
    
         
            -
                    # "Access-Control-Allow-Origin": the user should set that
         
     | 
| 
      
 137 
     | 
    
         
            +
                    # "Access-Control-Allow-Origin": the user should set that, see comment about "cors" above
         
     | 
| 
       107 
138 
     | 
    
         
             
                    {
         
     | 
| 
       108 
139 
     | 
    
         
             
                      # security relevant headers
         
     | 
| 
       109 
140 
     | 
    
         
             
                      "X-Frame-Options" => "DENY",
         
     | 
    
        data/lib/kirei/routing/router.rb
    CHANGED
    
    | 
         @@ -10,7 +10,7 @@ module Kirei 
     | 
|
| 
       10 
10 
     | 
    
         
             
                #
         
     | 
| 
       11 
11 
     | 
    
         
             
                # Router.add_routes([
         
     | 
| 
       12 
12 
     | 
    
         
             
                #   Route.new(
         
     | 
| 
       13 
     | 
    
         
            -
                #     verb:  
     | 
| 
      
 13 
     | 
    
         
            +
                #     verb: Verb::GET,
         
     | 
| 
       14 
14 
     | 
    
         
             
                #     path: "/livez",
         
     | 
| 
       15 
15 
     | 
    
         
             
                #     controller: Controllers::HealthController,
         
     | 
| 
       16 
16 
     | 
    
         
             
                #     action: "livez",
         
     | 
| 
         @@ -21,36 +21,6 @@ module Kirei 
     | 
|
| 
       21 
21 
     | 
    
         
             
                  extend T::Sig
         
     | 
| 
       22 
22 
     | 
    
         
             
                  include ::Singleton
         
     | 
| 
       23 
23 
     | 
    
         | 
| 
       24 
     | 
    
         
            -
                  class Verb < T::Enum
         
     | 
| 
       25 
     | 
    
         
            -
                    enums do
         
     | 
| 
       26 
     | 
    
         
            -
                      # idempotent
         
     | 
| 
       27 
     | 
    
         
            -
                      GET     = new("GET")
         
     | 
| 
       28 
     | 
    
         
            -
                      # non-idempotent
         
     | 
| 
       29 
     | 
    
         
            -
                      POST    = new("POST")
         
     | 
| 
       30 
     | 
    
         
            -
                      # idempotent
         
     | 
| 
       31 
     | 
    
         
            -
                      PUT     = new("PUT")
         
     | 
| 
       32 
     | 
    
         
            -
                      # non-idempotent
         
     | 
| 
       33 
     | 
    
         
            -
                      PATCH   = new("PATCH")
         
     | 
| 
       34 
     | 
    
         
            -
                      # non-idempotent
         
     | 
| 
       35 
     | 
    
         
            -
                      DELETE  = new("DELETE")
         
     | 
| 
       36 
     | 
    
         
            -
                      # idempotent
         
     | 
| 
       37 
     | 
    
         
            -
                      HEAD    = new("HEAD")
         
     | 
| 
       38 
     | 
    
         
            -
                      # idempotent
         
     | 
| 
       39 
     | 
    
         
            -
                      OPTIONS = new("OPTIONS")
         
     | 
| 
       40 
     | 
    
         
            -
                      # idempotent
         
     | 
| 
       41 
     | 
    
         
            -
                      TRACE   = new("TRACE")
         
     | 
| 
       42 
     | 
    
         
            -
                      # non-idempotent
         
     | 
| 
       43 
     | 
    
         
            -
                      CONNECT = new("CONNECT")
         
     | 
| 
       44 
     | 
    
         
            -
                    end
         
     | 
| 
       45 
     | 
    
         
            -
                  end
         
     | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
       47 
     | 
    
         
            -
                  class Route < T::Struct
         
     | 
| 
       48 
     | 
    
         
            -
                    const :verb, Verb
         
     | 
| 
       49 
     | 
    
         
            -
                    const :path, String
         
     | 
| 
       50 
     | 
    
         
            -
                    const :controller, T.class_of(Controller)
         
     | 
| 
       51 
     | 
    
         
            -
                    const :action, String
         
     | 
| 
       52 
     | 
    
         
            -
                  end
         
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
24 
     | 
    
         
             
                  RoutesHash = T.type_alias do
         
     | 
| 
       55 
25 
     | 
    
         
             
                    T::Hash[String, Route]
         
     | 
| 
       56 
26 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -0,0 +1,37 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # typed: strict
         
     | 
| 
      
 2 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Kirei
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Routing
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Verb < T::Enum
         
     | 
| 
      
 7 
     | 
    
         
            +
                  enums do
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # idempotent
         
     | 
| 
      
 9 
     | 
    
         
            +
                    GET     = new("GET")
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    # non-idempotent
         
     | 
| 
      
 12 
     | 
    
         
            +
                    POST    = new("POST")
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                    # idempotent
         
     | 
| 
      
 15 
     | 
    
         
            +
                    PUT     = new("PUT")
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    # non-idempotent
         
     | 
| 
      
 18 
     | 
    
         
            +
                    PATCH   = new("PATCH")
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    # non-idempotent
         
     | 
| 
      
 21 
     | 
    
         
            +
                    DELETE  = new("DELETE")
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                    # idempotent
         
     | 
| 
      
 24 
     | 
    
         
            +
                    HEAD    = new("HEAD")
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    # idempotent
         
     | 
| 
      
 27 
     | 
    
         
            +
                    OPTIONS = new("OPTIONS")
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    # idempotent
         
     | 
| 
      
 30 
     | 
    
         
            +
                    TRACE   = new("TRACE")
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                    # non-idempotent
         
     | 
| 
      
 33 
     | 
    
         
            +
                    CONNECT = new("CONNECT")
         
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
              end
         
     | 
| 
      
 37 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,53 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # typed: strict
         
     | 
| 
      
 2 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Kirei
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Services
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Result
         
     | 
| 
      
 7 
     | 
    
         
            +
                  extend T::Sig
         
     | 
| 
      
 8 
     | 
    
         
            +
                  extend T::Generic
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  ErrorType = type_member { { fixed: T::Array[Errors::JsonApiError] } }
         
     | 
| 
      
 11 
     | 
    
         
            +
                  ResultType = type_member { { upper: Object } }
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  sig do
         
     | 
| 
      
 14 
     | 
    
         
            +
                    params(
         
     | 
| 
      
 15 
     | 
    
         
            +
                      result: T.nilable(ResultType),
         
     | 
| 
      
 16 
     | 
    
         
            +
                      errors: ErrorType,
         
     | 
| 
      
 17 
     | 
    
         
            +
                    ).void
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
                  def initialize(result: nil, errors: [])
         
     | 
| 
      
 20 
     | 
    
         
            +
                    if (result && !errors.empty?) || (!result && errors.empty?)
         
     | 
| 
      
 21 
     | 
    
         
            +
                      raise ArgumentError, "Must provide either result or errors, got both or neither"
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                    @result = result
         
     | 
| 
      
 25 
     | 
    
         
            +
                    @errors = errors
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  sig { returns(T::Boolean) }
         
     | 
| 
      
 29 
     | 
    
         
            +
                  def success?
         
     | 
| 
      
 30 
     | 
    
         
            +
                    @errors.empty?
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  sig { returns(T::Boolean) }
         
     | 
| 
      
 34 
     | 
    
         
            +
                  def failed?
         
     | 
| 
      
 35 
     | 
    
         
            +
                    !success?
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  sig { returns(ResultType) }
         
     | 
| 
      
 39 
     | 
    
         
            +
                  def result
         
     | 
| 
      
 40 
     | 
    
         
            +
                    raise "Cannot call 'result' when there are errors" if failed?
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                    T.must(@result)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  sig { returns(ErrorType) }
         
     | 
| 
      
 46 
     | 
    
         
            +
                  def errors
         
     | 
| 
      
 47 
     | 
    
         
            +
                    raise "Cannot call 'errors' when there is a result" if success?
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                    @errors
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
              end
         
     | 
| 
      
 53 
     | 
    
         
            +
            end
         
     |