kirei 0.2.0 → 0.3.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 -24
- data/bin/kirei +1 -1
- data/kirei.gemspec +4 -3
- data/lib/cli/commands/new_app/base_directories.rb +1 -1
- data/lib/cli/commands/new_app/execute.rb +3 -2
- data/lib/cli/commands/new_app/files/app.rb +9 -3
- data/lib/cli/commands/new_app/files/config_ru.rb +1 -1
- data/lib/cli/commands/new_app/files/db_rake.rb +51 -3
- data/lib/cli/commands/new_app/files/irbrc.rb +1 -1
- data/lib/cli/commands/new_app/files/rakefile.rb +1 -1
- data/lib/cli/commands/new_app/files/routes.rb +49 -12
- data/lib/cli/commands/new_app/files/sorbet_config.rb +25 -0
- data/lib/kirei/app.rb +73 -56
- data/lib/kirei/controller.rb +44 -0
- data/lib/kirei/logger.rb +8 -8
- data/lib/kirei/{base_model.rb → model.rb} +5 -5
- data/lib/kirei/routing/base.rb +156 -0
- data/lib/kirei/routing/nilable_hooks_type.rb +10 -0
- data/lib/kirei/{middleware.rb → routing/rack_env_type.rb} +1 -10
- data/lib/kirei/routing/rack_response_type.rb +15 -0
- data/lib/kirei/routing/router.rb +86 -0
- data/lib/kirei/version.rb +1 -1
- data/lib/kirei.rb +27 -2
- data/sorbet/rbi/shims/base_model.rbi +1 -1
- metadata +29 -13
- data/lib/boot.rb +0 -23
- data/lib/kirei/app_base.rb +0 -72
- data/lib/kirei/base_controller.rb +0 -16
- data/lib/kirei/router.rb +0 -61
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: fb100f4fb7ab34f19caf8d428e373ac0e94230b126037147108b4826ca08b0c5
         | 
| 4 | 
            +
              data.tar.gz: 00efe991393cecf639ffe87ac5407fcf232d068940f55da8cc1696c08bbf75e6
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: bee31a71691f31363a7ffc4855c2ef9886f1ce3e545aafa5409a8064e7de79ec78670172b6c7e61348fb5b21f61779e080818e25077b9c300ec4f612dda83e28
         | 
| 7 | 
            +
              data.tar.gz: 1d4511cbd01bc7f2eb5f03bd3032079232650b30226ca510dce48a07ee4fc7814e0e6e9ac25ba5df1cd14a11ee957a508d2cbededfbf29ee7806b1840d3455b9
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # Kirei
         | 
| 2 2 |  | 
| 3 | 
            -
            Kirei is a strictly typed Ruby micro/REST-framework for building scalable and performant APIs. It is built from the ground up to be clean and easy to use. Kirei is based on [Sequel](https://github.com/jeremyevans/sequel) as an ORM, [Sorbet](https://github.com/sorbet/sorbet) for typing, and [ | 
| 3 | 
            +
            Kirei is a strictly typed Ruby micro/REST-framework for building scalable and performant APIs. It is built from the ground up to be clean and easy to use. Kirei is based on [Sequel](https://github.com/jeremyevans/sequel) as an ORM, [Sorbet](https://github.com/sorbet/sorbet) for typing, and [Rack](https://github.com/rack/rack) as web server interface. It strives to have zero magic and to be as explicit as possible.
         | 
| 4 4 |  | 
| 5 5 | 
             
            Kirei's main advantages over other frameworks are its strict typing, low memory footprint, and build-in high-performance logging and metric-tracking toolkits. It is opiniated in terms of tooling, allowing you to focus on your core-business. It is a great choice for building APIs that need to scale.
         | 
| 6 6 |  | 
| @@ -53,12 +53,12 @@ Find a test app in the [spec/test_app](spec/test_app) directory. It is a fully f | |
| 53 53 |  | 
| 54 54 | 
             
            #### Models
         | 
| 55 55 |  | 
| 56 | 
            -
            All models must inherit from `T::Struct` and include `Kirei:: | 
| 56 | 
            +
            All models must inherit from `T::Struct` and include `Kirei::Model`. They must implement `id` which must hold the primary key of the table. The primary key must be named `id` and be of type `T.any(String, Integer)`.
         | 
| 57 57 |  | 
| 58 58 | 
             
            ```ruby
         | 
| 59 59 | 
             
            class User < T::Struct
         | 
| 60 60 | 
             
              extend T::Sig
         | 
| 61 | 
            -
              include Kirei:: | 
| 61 | 
            +
              include Kirei::Model
         | 
| 62 62 |  | 
| 63 63 | 
             
              const :id, T.any(String, Integer)
         | 
| 64 64 | 
             
              const :name, String
         | 
| @@ -76,12 +76,21 @@ user.name         # => 'John' | |
| 76 76 | 
             
            updated_user.name # => 'Johnny'
         | 
| 77 77 | 
             
            ```
         | 
| 78 78 |  | 
| 79 | 
            +
            Delete keeps the original object intact. Returns `true` if the record was deleted. Calling delete multiple times will return `false` after the first (successful) call.
         | 
| 80 | 
            +
             | 
| 81 | 
            +
            ```ruby
         | 
| 82 | 
            +
            success = user.delete # => T::Boolean
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            # or delete by any query:
         | 
| 85 | 
            +
            User.db.where('...').delete # => Integer, number of deleted records
         | 
| 86 | 
            +
            ```
         | 
| 87 | 
            +
             | 
| 79 88 | 
             
            To build more complex queries, Sequel can be used directly:
         | 
| 80 89 |  | 
| 81 90 | 
             
            ```ruby
         | 
| 82 91 | 
             
            query = User.db.where({ name: 'John' })
         | 
| 83 | 
            -
            query = query.where('...')
         | 
| 84 | 
            -
            query = query.limit(10) | 
| 92 | 
            +
            query = query.where('...') # "query" is a 'Sequel::Dataset' that you can chain as you like
         | 
| 93 | 
            +
            query = query.limit(10)
         | 
| 85 94 |  | 
| 86 95 | 
             
            users = User.resolve(query)            # T::Array[User]
         | 
| 87 96 | 
             
            first_user = User.resolve_first(query) # T.nilable(User)
         | 
| @@ -121,11 +130,15 @@ bundle exec rake db:drop | |
| 121 130 | 
             
            # apply all pending migrations
         | 
| 122 131 | 
             
            bundle exec rake db:migrate
         | 
| 123 132 |  | 
| 133 | 
            +
            # annotate the models with the schema
         | 
| 134 | 
            +
            # this runs automatically after each migration
         | 
| 135 | 
            +
            bundle exec rake db:annotate
         | 
| 136 | 
            +
             | 
| 124 137 | 
             
            # roll back the last n migration
         | 
| 125 138 | 
             
            STEPS=1 bundle exec rake db:rollback
         | 
| 126 139 |  | 
| 127 140 | 
             
            # run db/seeds.rb to seed the database
         | 
| 128 | 
            -
            bundle exec rake db: | 
| 141 | 
            +
            bundle exec rake db:seed
         | 
| 129 142 |  | 
| 130 143 | 
             
            # scaffold a new migration file
         | 
| 131 144 | 
             
            bundle exec rake 'db:migration[CreateAirports]'
         | 
| @@ -138,21 +151,24 @@ Define routes anywhere in your app; by convention, they are defined in `config/r | |
| 138 151 | 
             
            ```ruby
         | 
| 139 152 | 
             
            # config/routes.rb
         | 
| 140 153 |  | 
| 141 | 
            -
            Kirei:: | 
| 142 | 
            -
               | 
| 143 | 
            -
                 | 
| 144 | 
            -
             | 
| 145 | 
            -
             | 
| 146 | 
            -
             | 
| 147 | 
            -
             | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
             | 
| 151 | 
            -
             | 
| 152 | 
            -
             | 
| 153 | 
            -
             | 
| 154 | 
            -
             | 
| 155 | 
            -
             | 
| 154 | 
            +
            module Kirei::Routing
         | 
| 155 | 
            +
              Router.add_routes(
         | 
| 156 | 
            +
                [
         | 
| 157 | 
            +
                  Router::Route.new(
         | 
| 158 | 
            +
                    verb: Router::Verb::GET,
         | 
| 159 | 
            +
                    path: "/livez",
         | 
| 160 | 
            +
                    controller: Controllers::Health,
         | 
| 161 | 
            +
                    action: "livez",
         | 
| 162 | 
            +
                  ),
         | 
| 163 | 
            +
                  Router::Route.new(
         | 
| 164 | 
            +
                    verb: Router::Verb::GET,
         | 
| 165 | 
            +
                    path: "/airports",
         | 
| 166 | 
            +
                    controller: Controllers::Airports,
         | 
| 167 | 
            +
                    action: "index",
         | 
| 168 | 
            +
                  ),
         | 
| 169 | 
            +
                ],
         | 
| 170 | 
            +
              )
         | 
| 171 | 
            +
            end
         | 
| 156 172 | 
             
            ```
         | 
| 157 173 |  | 
| 158 174 | 
             
            #### Controllers
         | 
| @@ -161,12 +177,12 @@ Controllers can be defined anywhere; by convention, they are defined in the `app | |
| 161 177 |  | 
| 162 178 | 
             
            ```ruby
         | 
| 163 179 | 
             
            module Controllers
         | 
| 164 | 
            -
              class Airports < Kirei:: | 
| 180 | 
            +
              class Airports < Kirei::Controller
         | 
| 165 181 | 
             
                extend T::Sig
         | 
| 166 182 |  | 
| 167 | 
            -
                sig { returns( | 
| 183 | 
            +
                sig { returns(T.anything) }
         | 
| 168 184 | 
             
                def index
         | 
| 169 | 
            -
                  airports = Airport.all
         | 
| 185 | 
            +
                  airports = Airport.all # T::Array[Airport]
         | 
| 170 186 |  | 
| 171 187 | 
             
                  # or use a serializer
         | 
| 172 188 | 
             
                  data = Oj.dump(airports.map(&:serialize))
         | 
    
        data/bin/kirei
    CHANGED
    
    
    
        data/kirei.gemspec
    CHANGED
    
    | @@ -13,11 +13,11 @@ Gem::Specification.new do |spec| | |
| 13 13 | 
             
                "oss@dbl.works",
         | 
| 14 14 | 
             
              ]
         | 
| 15 15 |  | 
| 16 | 
            -
              spec.summary = "Kirei is a  | 
| 16 | 
            +
              spec.summary = "Kirei is a typed Ruby micro/REST-framework for building scalable and performant microservices."
         | 
| 17 17 | 
             
              spec.description = <<~TXT
         | 
| 18 | 
            -
                Kirei is a  | 
| 18 | 
            +
                Kirei is a Ruby micro/REST-framework for building scalable and performant microservices.
         | 
| 19 19 | 
             
                It is built from the ground up to be clean and easy to use.
         | 
| 20 | 
            -
                 | 
| 20 | 
            +
                It is a Rack app, and uses Sorbet for typing, Sequel as an ORM, Zeitwerk for autoloading, and Puma as a web server.
         | 
| 21 21 | 
             
                It strives to have zero magic and to be as explicit as possible.
         | 
| 22 22 | 
             
              TXT
         | 
| 23 23 | 
             
              spec.homepage = "https://github.com/swiknaba/kirei"
         | 
| @@ -47,6 +47,7 @@ Gem::Specification.new do |spec| | |
| 47 47 | 
             
              spec.add_dependency "oj", "~> 3.0"
         | 
| 48 48 | 
             
              spec.add_dependency "sorbet-runtime", "~> 0.5"
         | 
| 49 49 | 
             
              spec.add_dependency "tzinfo-data", "~> 1.0" # for containerized environments, e.g. on AWS ECS
         | 
| 50 | 
            +
              spec.add_dependency "zeitwerk", "~> 2.5"
         | 
| 50 51 |  | 
| 51 52 | 
             
              # Web server & routing
         | 
| 52 53 | 
             
              spec.add_dependency "puma", "~> 6.0"
         | 
| @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            # typed:  | 
| 1 | 
            +
            # typed: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            require "fileutils"
         | 
| 4 4 |  | 
| @@ -13,7 +13,8 @@ module Cli | |
| 13 13 | 
             
                      Files::DbRake.call(app_name)
         | 
| 14 14 | 
             
                      Files::Irbrc.call
         | 
| 15 15 | 
             
                      Files::Rakefile.call
         | 
| 16 | 
            -
                      Files::Routes.call
         | 
| 16 | 
            +
                      Files::Routes.call(app_name)
         | 
| 17 | 
            +
                      Files::SorbetConfig.call
         | 
| 17 18 |  | 
| 18 19 | 
             
                      Kirei::Logger.logger.info(
         | 
| 19 20 | 
             
                        "Kirei app '#{app_name}' scaffolded successfully!",
         | 
| @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            # typed:  | 
| 1 | 
            +
            # typed: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Cli
         | 
| 4 4 | 
             
              module Commands
         | 
| @@ -29,15 +29,21 @@ module Cli | |
| 29 29 | 
             
                          Dir[File.join(__dir__, "config/initializers", "*.rb")].each { require(_1) }
         | 
| 30 30 |  | 
| 31 31 | 
             
                          # Fourth: load all application code
         | 
| 32 | 
            -
                           | 
| 32 | 
            +
                          loader = Zeitwerk::Loader.new
         | 
| 33 | 
            +
                          loader.tag = File.basename(__FILE__, ".rb")
         | 
| 34 | 
            +
                          loader.push_dir("#{File.dirname(__FILE__)}/app")
         | 
| 35 | 
            +
                          loader.push_dir("#{File.dirname(__FILE__)}/app/models") # make models a root namespace so we don't infer a `Models::` module
         | 
| 36 | 
            +
                          loader.setup
         | 
| 33 37 |  | 
| 34 38 | 
             
                          # Fifth: load configs
         | 
| 35 39 | 
             
                          Dir[File.join(__dir__, "config", "*.rb")].each { require(_1) }
         | 
| 36 40 |  | 
| 37 | 
            -
                          class #{app_name} < Kirei:: | 
| 41 | 
            +
                          class #{app_name} < Kirei::App
         | 
| 38 42 | 
             
                            # Kirei configuration
         | 
| 39 43 | 
             
                            config.app_name = "#{snake_case_app_name}"
         | 
| 40 44 | 
             
                          end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                          loader.eager_load
         | 
| 41 47 | 
             
                        RUBY
         | 
| 42 48 | 
             
                      end
         | 
| 43 49 | 
             
                    end
         | 
| @@ -21,6 +21,7 @@ module Cli | |
| 21 21 | 
             
                          #
         | 
| 22 22 | 
             
                          #   CREATE DATABASE #{db_name}_${environment};
         | 
| 23 23 |  | 
| 24 | 
            +
                          require 'zeitwerk/inflector'
         | 
| 24 25 | 
             
                          require_relative "../../app"
         | 
| 25 26 |  | 
| 26 27 | 
             
                          namespace :db do
         | 
| @@ -30,7 +31,7 @@ module Cli | |
| 30 31 | 
             
                              envs = ENV.key?("RACK_ENV") ? [ENV.fetch("RACK_ENV")] : %w[development test]
         | 
| 31 32 | 
             
                              envs.each do |env|
         | 
| 32 33 | 
             
                                ENV["RACK_ENV"] = env
         | 
| 33 | 
            -
                                db_name = "#{db_name}_ | 
| 34 | 
            +
                                db_name = "#{db_name}_\#{env}"
         | 
| 34 35 | 
             
                                puts("Creating database \#{db_name}...")
         | 
| 35 36 |  | 
| 36 37 | 
             
                                reset_memoized_class_level_instance_vars(#{app_name})
         | 
| @@ -84,6 +85,8 @@ module Cli | |
| 84 85 | 
             
                                current_version = db[:schema_migrations].order(:filename).last[:filename].to_i
         | 
| 85 86 | 
             
                                puts "Migrated \#{db_name} to version \#{current_version}!"
         | 
| 86 87 | 
             
                              end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                              Rake::Task["db:annotate"].invoke
         | 
| 87 90 | 
             
                            end
         | 
| 88 91 |  | 
| 89 92 | 
             
                            desc "Rollback the last migration"
         | 
| @@ -128,7 +131,7 @@ module Cli | |
| 128 131 | 
             
                              migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S")
         | 
| 129 132 |  | 
| 130 133 | 
             
                              # Sanitize and format the migration name
         | 
| 131 | 
            -
                              formatted_name = args[:name].to_s.gsub(/([a-z])([A-Z])/, ' | 
| 134 | 
            +
                              formatted_name = args[:name].to_s.gsub(/([a-z])([A-Z])/, '\\1_\\2').downcase
         | 
| 132 135 |  | 
| 133 136 | 
             
                              # Combine them to create the filename
         | 
| 134 137 | 
             
                              filename = "\#{migration_number}_\#{formatted_name}.rb"
         | 
| @@ -136,7 +139,7 @@ module Cli | |
| 136 139 |  | 
| 137 140 | 
             
                              # Define the content of the migration file
         | 
| 138 141 | 
             
                              content = <<~MIGRATION
         | 
| 139 | 
            -
                                # typed:  | 
| 142 | 
            +
                                # typed: false
         | 
| 140 143 | 
             
                                # frozen_string_literal: true
         | 
| 141 144 |  | 
| 142 145 | 
             
                                Sequel.migration do
         | 
| @@ -155,6 +158,38 @@ module Cli | |
| 155 158 |  | 
| 156 159 | 
             
                              puts "Generated migration: db/migrate/\#{filename}"
         | 
| 157 160 | 
             
                            end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                            desc "Write the table schema to each model file, or a single file if filename (without extension) is provided"
         | 
| 163 | 
            +
                            task :annotate, [:model_file_name] do |_t, args|
         | 
| 164 | 
            +
                              require "fileutils"
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                              db = #{app_name}.raw_db_connection
         | 
| 167 | 
            +
                              model_file_name = args[:model_file_name]&.to_s
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                              models_dir = #{app_name}.root
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                              Dir.glob("app/models/*.rb").each do |model_file|
         | 
| 172 | 
            +
                                next if !model_file_name.nil? && model_file == model_file_name
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                                model_path = File.expand_path(model_file, models_dir)
         | 
| 175 | 
            +
                                model_name = Zeitwerk::Inflector.new.camelize(File.basename(model_file, ".rb"), model_path)
         | 
| 176 | 
            +
                                model_klass = Object.const_get(model_name)
         | 
| 177 | 
            +
                                table_name = model_klass.table_name
         | 
| 178 | 
            +
                                schema = db.schema(table_name)
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                                schema_comments = format_schema_comments(table_name, schema)
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                                file_contents = File.read(model_path)
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                                # Remove existing schema info comments if present
         | 
| 185 | 
            +
                                updated_contents = file_contents.sub(/# == Schema Info\\n(.*?)(\\n#\\n)?\\n(?=\\s*class)/m, "")
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                                # Insert the new schema comments before the class definition
         | 
| 188 | 
            +
                                modified_contents = updated_contents.sub(/(\A|\\n)(class \#{model_name})/m, "\\\\1\#{schema_comments}\\n\\n\\\\2")
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                                File.write(model_path, modified_contents)
         | 
| 191 | 
            +
                              end
         | 
| 192 | 
            +
                            end
         | 
| 158 193 | 
             
                          end
         | 
| 159 194 |  | 
| 160 195 | 
             
                          def reset_memoized_class_level_instance_vars(app)
         | 
| @@ -167,6 +202,19 @@ module Cli | |
| 167 202 | 
             
                            end
         | 
| 168 203 | 
             
                          end
         | 
| 169 204 |  | 
| 205 | 
            +
                          def format_schema_comments(table_name, schema)
         | 
| 206 | 
            +
                            lines = ["# == Schema Info", "#", "# Table name: \#{table_name}", "#"]
         | 
| 207 | 
            +
                            schema.each do |column|
         | 
| 208 | 
            +
                              name, info = column
         | 
| 209 | 
            +
                              type = "\#{info[:db_type]}(\#{info[:max_length]})" if info[:max_length]
         | 
| 210 | 
            +
                              type ||= info[:db_type]
         | 
| 211 | 
            +
                              null = info[:allow_null] ? 'null' : 'not null'
         | 
| 212 | 
            +
                              primary_key = info[:primary_key] ? ', primary key' : ''
         | 
| 213 | 
            +
                              lines << "#  \#{name.to_s.ljust(20)}:\#{type}    \#{null}\#{primary_key}"
         | 
| 214 | 
            +
                            end
         | 
| 215 | 
            +
                            lines.join("\\n") + "\\n#"
         | 
| 216 | 
            +
                          end
         | 
| 217 | 
            +
             | 
| 170 218 | 
             
                        RUBY
         | 
| 171 219 | 
             
                      end
         | 
| 172 220 | 
             
                    end
         | 
| @@ -1,27 +1,64 @@ | |
| 1 | 
            -
            # typed:  | 
| 1 | 
            +
            # typed: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Cli
         | 
| 4 4 | 
             
              module Commands
         | 
| 5 5 | 
             
                module NewApp
         | 
| 6 6 | 
             
                  module Files
         | 
| 7 7 | 
             
                    class Routes
         | 
| 8 | 
            -
                      def self.call
         | 
| 9 | 
            -
                        File.write("config/routes.rb",  | 
| 8 | 
            +
                      def self.call(app_name)
         | 
| 9 | 
            +
                        File.write("config/routes.rb", router)
         | 
| 10 | 
            +
                        File.write("app/controllers/base.rb", base_controller)
         | 
| 11 | 
            +
                        File.write("app/controllers/health.rb", health_controller(app_name))
         | 
| 10 12 | 
             
                      end
         | 
| 11 13 |  | 
| 12 | 
            -
                      def self. | 
| 14 | 
            +
                      def self.router
         | 
| 13 15 | 
             
                        <<~RUBY
         | 
| 14 16 | 
             
                          # typed: strict
         | 
| 15 17 | 
             
                          # frozen_string_literal: true
         | 
| 16 18 |  | 
| 17 | 
            -
                          Kirei:: | 
| 18 | 
            -
                             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 19 | 
            +
                          module Kirei::Routing
         | 
| 20 | 
            +
                            Router.add_routes(
         | 
| 21 | 
            +
                              [
         | 
| 22 | 
            +
                                Router::Route.new(
         | 
| 23 | 
            +
                                  verb: Router::Verb::GET,
         | 
| 24 | 
            +
                                  path: "/livez",
         | 
| 25 | 
            +
                                  controller: Controllers::Health,
         | 
| 26 | 
            +
                                  action: "livez",
         | 
| 27 | 
            +
                                ),
         | 
| 28 | 
            +
                              ],
         | 
| 29 | 
            +
                            )
         | 
| 30 | 
            +
                          end
         | 
| 31 | 
            +
                        RUBY
         | 
| 32 | 
            +
                      end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      def self.base_controller
         | 
| 35 | 
            +
                        <<~RUBY
         | 
| 36 | 
            +
                          # typed: strict
         | 
| 37 | 
            +
                          # frozen_string_literal: true
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                          module Controllers
         | 
| 40 | 
            +
                            class Base < Kirei::Controller
         | 
| 41 | 
            +
                              extend T::Sig
         | 
| 42 | 
            +
                            end
         | 
| 43 | 
            +
                          end
         | 
| 44 | 
            +
                        RUBY
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                      def self.health_controller(app_name)
         | 
| 48 | 
            +
                        <<~RUBY
         | 
| 49 | 
            +
                          # typed: strict
         | 
| 50 | 
            +
                          # frozen_string_literal: true
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                          module Controllers
         | 
| 53 | 
            +
                            class Health < Base
         | 
| 54 | 
            +
                              sig { returns(T.anything) }
         | 
| 55 | 
            +
                              def livez
         | 
| 56 | 
            +
                                #{app_name}.config.logger.info("Health check")
         | 
| 57 | 
            +
                                #{app_name}.config.logger.info(params.inspect)
         | 
| 58 | 
            +
                                render(#{app_name}.version, status: 200)
         | 
| 59 | 
            +
                              end
         | 
| 60 | 
            +
                            end
         | 
| 61 | 
            +
                          end
         | 
| 25 62 | 
             
                        RUBY
         | 
| 26 63 | 
             
                      end
         | 
| 27 64 | 
             
                    end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            # typed: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Cli
         | 
| 4 | 
            +
              module Commands
         | 
| 5 | 
            +
                module NewApp
         | 
| 6 | 
            +
                  module Files
         | 
| 7 | 
            +
                    class SorbetConfig
         | 
| 8 | 
            +
                      def self.call
         | 
| 9 | 
            +
                        File.write("sorbet/config", content)
         | 
| 10 | 
            +
                      end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      def self.content
         | 
| 13 | 
            +
                        <<~TXT
         | 
| 14 | 
            +
                          --dir
         | 
| 15 | 
            +
                          .
         | 
| 16 | 
            +
                          --ignore=vendor/
         | 
| 17 | 
            +
                          --ignore=spec/
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                        TXT
         | 
| 20 | 
            +
                      end
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
    
        data/lib/kirei/app.rb
    CHANGED
    
    | @@ -1,72 +1,89 @@ | |
| 1 1 | 
             
            # typed: strict
         | 
| 2 2 | 
             
            # frozen_string_literal: true
         | 
| 3 3 |  | 
| 4 | 
            -
             | 
| 4 | 
            +
            module Kirei
         | 
| 5 | 
            +
              class App < Routing::Base
         | 
| 6 | 
            +
                class << self
         | 
| 7 | 
            +
                  extend T::Sig
         | 
| 5 8 |  | 
| 6 | 
            -
            # | 
| 9 | 
            +
                  #
         | 
| 10 | 
            +
                  # convenience method since "Kirei.configuration" must be nilable since it is nil
         | 
| 11 | 
            +
                  # at the beginning of initilization of the app
         | 
| 12 | 
            +
                  #
         | 
| 13 | 
            +
                  sig { returns(Kirei::Config) }
         | 
| 14 | 
            +
                  def config
         | 
| 15 | 
            +
                    T.must(Kirei.configuration)
         | 
| 16 | 
            +
                  end
         | 
| 7 17 |  | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 18 | 
            +
                  sig { returns(Pathname) }
         | 
| 19 | 
            +
                  def root
         | 
| 20 | 
            +
                    defined?(::APP_ROOT) ? Pathname.new(::APP_ROOT) : Pathname.new(Dir.pwd)
         | 
| 21 | 
            +
                  end
         | 
| 12 22 |  | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
                   | 
| 16 | 
            -
                   | 
| 17 | 
            -
             | 
| 23 | 
            +
                  #
         | 
| 24 | 
            +
                  # Returns the version of the app. It checks in the following order:
         | 
| 25 | 
            +
                  # * ENV["APP_VERSION"]
         | 
| 26 | 
            +
                  # * ENV["GIT_SHA"]
         | 
| 27 | 
            +
                  # * `git rev-parse --short HEAD`
         | 
| 28 | 
            +
                  #
         | 
| 29 | 
            +
                  sig { returns(String) }
         | 
| 30 | 
            +
                  def version
         | 
| 31 | 
            +
                    @version = T.let(@version, T.nilable(String))
         | 
| 32 | 
            +
                    @version ||= ENV.fetch("APP_VERSION", nil)
         | 
| 33 | 
            +
                    @version ||= ENV.fetch("GIT_SHA", nil)
         | 
| 34 | 
            +
                    @version ||= T.must(
         | 
| 35 | 
            +
                      `command -v git && git rev-parse --short HEAD`.to_s.split("\n").last,
         | 
| 36 | 
            +
                    ).freeze # localhost
         | 
| 37 | 
            +
                  end
         | 
| 18 38 |  | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 39 | 
            +
                  #
         | 
| 40 | 
            +
                  # Returns ENV["RACK_ENV"] or "development" if it is not set
         | 
| 41 | 
            +
                  #
         | 
| 42 | 
            +
                  sig { returns(String) }
         | 
| 43 | 
            +
                  def environment
         | 
| 44 | 
            +
                    ENV.fetch("RACK_ENV", "development")
         | 
| 45 | 
            +
                  end
         | 
| 21 46 |  | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
                   | 
| 25 | 
            -
                   | 
| 26 | 
            -
                   | 
| 27 | 
            -
                   | 
| 28 | 
            -
             | 
| 47 | 
            +
                  #
         | 
| 48 | 
            +
                  # Returns the name of the database based on the app name and the environment,
         | 
| 49 | 
            +
                  # e.g. "myapp_development"
         | 
| 50 | 
            +
                  #
         | 
| 51 | 
            +
                  sig { returns(String) }
         | 
| 52 | 
            +
                  def default_db_name
         | 
| 53 | 
            +
                    @default_db_name ||= T.let("#{config.app_name}_#{environment}".freeze, T.nilable(String))
         | 
| 54 | 
            +
                  end
         | 
| 29 55 |  | 
| 30 | 
            -
                   | 
| 31 | 
            -
                   | 
| 56 | 
            +
                  #
         | 
| 57 | 
            +
                  # Returns the database URL based on the DATABASE_URL environment variable or
         | 
| 58 | 
            +
                  # a default value based on the default_db_name
         | 
| 59 | 
            +
                  #
         | 
| 60 | 
            +
                  sig { returns(String) }
         | 
| 61 | 
            +
                  def default_db_url
         | 
| 62 | 
            +
                    @default_db_url ||= T.let(
         | 
| 63 | 
            +
                      ENV.fetch("DATABASE_URL", "postgresql://localhost:5432/#{default_db_name}"),
         | 
| 64 | 
            +
                      T.nilable(String),
         | 
| 65 | 
            +
                    )
         | 
| 66 | 
            +
                  end
         | 
| 32 67 |  | 
| 33 | 
            -
                   | 
| 34 | 
            -
             | 
| 35 | 
            -
                     | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 68 | 
            +
                  sig { returns(Sequel::Database) }
         | 
| 69 | 
            +
                  def raw_db_connection
         | 
| 70 | 
            +
                    @raw_db_connection = T.let(@raw_db_connection, T.nilable(Sequel::Database))
         | 
| 71 | 
            +
                    return @raw_db_connection unless @raw_db_connection.nil?
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    # calling "Sequel.connect" creates a new connection
         | 
| 74 | 
            +
                    @raw_db_connection = Sequel.connect(App.config.db_url || default_db_url)
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    config.db_extensions.each do |ext|
         | 
| 77 | 
            +
                      T.cast(@raw_db_connection, Sequel::Database).extension(ext)
         | 
| 39 78 | 
             
                    end
         | 
| 40 | 
            -
                  else
         | 
| 41 | 
            -
                    # TODO: based on content-type, parse the body differently
         | 
| 42 | 
            -
                    #       build-in support for JSON & XML
         | 
| 43 | 
            -
                    body = T.cast(env.fetch("rack.input"), T.any(IO, StringIO))
         | 
| 44 | 
            -
                    res = Oj.load(body.read, Kirei::OJ_OPTIONS)
         | 
| 45 | 
            -
                    body.rewind # TODO: maybe don't rewind if we don't need to?
         | 
| 46 | 
            -
                    T.cast(res, T::Hash[String, T.untyped])
         | 
| 47 | 
            -
                  end
         | 
| 48 79 |  | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 80 | 
            +
                    if config.db_extensions.include?(:pg_json)
         | 
| 81 | 
            +
                      # https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb#L8
         | 
| 82 | 
            +
                      @raw_db_connection.wrap_json_primitives = true
         | 
| 83 | 
            +
                    end
         | 
| 52 84 |  | 
| 53 | 
            -
             | 
| 54 | 
            -
                   | 
| 55 | 
            -
                    status: Integer,
         | 
| 56 | 
            -
                    body: String,
         | 
| 57 | 
            -
                    headers: T::Hash[String, String],
         | 
| 58 | 
            -
                  ).returns(RackResponseType)
         | 
| 59 | 
            -
                end
         | 
| 60 | 
            -
                def render(status:, body:, headers: {})
         | 
| 61 | 
            -
                  # merge default headers
         | 
| 62 | 
            -
                  # support a "type" to set content-type header? (or default to json, and users must set the header themselves for other types?)
         | 
| 63 | 
            -
                  [
         | 
| 64 | 
            -
                    status,
         | 
| 65 | 
            -
                    headers,
         | 
| 66 | 
            -
                    [body],
         | 
| 67 | 
            -
                  ]
         | 
| 85 | 
            +
                    @raw_db_connection
         | 
| 86 | 
            +
                  end
         | 
| 68 87 | 
             
                end
         | 
| 69 88 | 
             
              end
         | 
| 70 89 | 
             
            end
         | 
| 71 | 
            -
             | 
| 72 | 
            -
            # rubocop:enable Metrics/AbcSize, Layout/LineLength
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            # typed: strict
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Kirei
         | 
| 5 | 
            +
              class Controller < Routing::Base
         | 
| 6 | 
            +
                class << self
         | 
| 7 | 
            +
                  extend T::Sig
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  sig { returns(Routing::NilableHooksType) }
         | 
| 10 | 
            +
                  attr_reader :before_hooks, :after_hooks
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                extend T::Sig
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                # Statements to be executed before every action.
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # In development mode, Rack Reloader might reload this file causing
         | 
| 19 | 
            +
                # the before hooks to be executed multiple times.
         | 
| 20 | 
            +
                #
         | 
| 21 | 
            +
                sig do
         | 
| 22 | 
            +
                  params(
         | 
| 23 | 
            +
                    block: T.nilable(T.proc.void),
         | 
| 24 | 
            +
                  ).void
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
                def self.before(&block)
         | 
| 27 | 
            +
                  @before_hooks ||= T.let(Set.new, Routing::NilableHooksType)
         | 
| 28 | 
            +
                  @before_hooks.add(block) if block
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                #
         | 
| 32 | 
            +
                # Statements to be executed after every action.
         | 
| 33 | 
            +
                #
         | 
| 34 | 
            +
                sig do
         | 
| 35 | 
            +
                  params(
         | 
| 36 | 
            +
                    block: T.nilable(T.proc.void),
         | 
| 37 | 
            +
                  ).void
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
                def self.after(&block)
         | 
| 40 | 
            +
                  @after_hooks ||= T.let(Set.new, Routing::NilableHooksType)
         | 
| 41 | 
            +
                  @after_hooks.add(block) if block
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         |