module Pakyow

Pakyow environment for running one or more rack apps. Multiple apps can be mounted in the environment, each one handling requests at some path.

Pakyow.configure do
  mount Pakyow::Application, at: "/"
end

Configuration

The environment can be configured

Pakyow.configure do
  config.server.port = 2001
end

It's possible to configure environments differently.

Pakyow.configure :development do
  config.server.host = "pakyow.dev"
end

@see Support::Configurable

Hooks

Hooks can be defined for the following events:

- load
- configure
- setup
- boot
- shutdown
- run

Here's how to log a message after boot:

Pakyow.after "boot" do
  logger.info "booted"
end

@see Support::Hookable

Logging

The environment contains a global general-purpose logger. It also provides a {RequestLogger} instance to each app for logging during a request.

Setup & Running

The environment can be setup and then run.

Pakyow.setup(env: :development).run

Constants

VERSION

Pakyow's current version.

Attributes

env[R]

Name of the environment

error[R]

Any error encountered during the boot process

Public Class Methods

app(app_name, path: "/", without: [], only: nil, mount: true, &block) click to toggle source
# File lib/pakyow/environment.rb, line 289
def app(app_name, path: "/", without: [], only: nil, mount: true, &block)
  app_name = app_name.to_sym

  if booted?
    @apps.find { |app|
      app.config.name == app_name
    }
  else
    local_frameworks = (only || frameworks.keys) - Array.ensure(without)

    Pakyow::Application.make(Support::ObjectName.namespace(app_name, "application")) {
      config.name = app_name
      include_frameworks(*local_frameworks)
    }.tap do |app|
      app.define(&block) if block_given?
      mount(app, at: path) if mount
    end
  end
end
boot(unsafe: false) click to toggle source

Boots the environment without running it.

# File lib/pakyow/environment.rb, line 248
def boot(unsafe: false)
  ensure_setup_succeeded

  performing :boot do
    # Tasks should only be available before boot.
    #
    @tasks = [] unless unsafe

    # Mount each app.
    #
    @apps = mounts.map { |mount|
      initialize_app_for_mount(mount)
    }

    # Create the callable pipeline.
    #
    @pipeline = Pakyow.__pipeline.callable(self)

    # Set the environment as booted ahead of telling each app that it is booted. This allows an
    # app's after boot hook to access the booted app through `Pakyow.app`.
    #
    @booted = true

    # Now tell each app that it has been booted.
    #
    @apps.select { |app| app.respond_to?(:booted) }.each(&:booted)
  end

  if config.freeze_on_boot
    deep_freeze unless unsafe
  end

  self
rescue StandardError => error
  handle_boot_failure(error)
end
booted?() click to toggle source

Returns true if the environment has booted.

# File lib/pakyow/environment.rb, line 242
def booted?
  @booted == true
end
call(input) click to toggle source
# File lib/pakyow/environment.rb, line 313
def call(input)
  config.connection_class.new(input).yield_self { |connection|
    Async(logger: connection.logger) {
      # Set the request logger as a thread-local variable for when there's no other way to access
      # it. This originated when looking for a way to log queries with the request logger. By
      # setting the request logger for the current connection as thread-local we can create a
      # connection pointing to `Pakyow.logger`, an instance of `Pakyow::Logger::ThreadLocal`. The
      # thread local logger decides at the time of logging which logger to use based on an
      # available context, falling back to `Pakyow.global_logger`. This gets us around needing to
      # configure a connection per request, altering Sequel's internals, and other oddities.
      #
      # Pakyow is designed so that the connection object and its logger should always be available
      # anywhere you need it. If it isn't, reconsider the design before using the thread local.
      #
      Thread.current[:pakyow_logger] = connection.logger

      catch :halt do
        @pipeline.call(connection)
      end
    }.wait
  }.finalize
rescue StandardError => error
  Pakyow.logger.houston(error)

  Async::HTTP::Protocol::Response.new(
    nil, 500, {}, Async::HTTP::Body::Buffered.wrap(
      StringIO.new("500 Low-Level Server Error")
    )
  )
end
env?(name) click to toggle source
# File lib/pakyow/environment.rb, line 309
def env?(name)
  env == name.to_sym
end
global_logger() click to toggle source

Global log output.

Builds and returns a default global output that's replaced in `setup`.

# File lib/pakyow/environment.rb, line 147
def global_logger
  unless defined?(@global_logger)
    require "pakyow/logger/formatters/human"
    @global_logger = Logger::Formatters::Human.new(
      Logger::Destination.new(:stdout, $stdout)
    )
  end

  @global_logger
end
info() click to toggle source

Returns information about the environment.

# File lib/pakyow/info.rb, line 8
def self.info
  {
    versions: {
      ruby: "v#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})",
      pakyow: "v#{VERSION}"
    },

    apps: Pakyow.mounts.map { |mount|
      {
        mount_path: mount[:path],
        class: mount[:app].to_s,
        reference: mount[:app].config.name.inspect,
        frameworks: mount[:app].config.loaded_frameworks,
        app_root: File.expand_path(mount[:app].config.root)
      }
    }
  }
end
initialize_app_for_mount(mount) click to toggle source

@api private

# File lib/pakyow/environment.rb, line 357
def initialize_app_for_mount(mount)
  if mount[:app].ancestors.include?(Pakyow::Application)
    mount[:app].new(env, mount_path: mount[:path], &mount[:block])
  else
    mount[:app].new
  end
end
load() click to toggle source

Loads the Pakyow environment for the current project.

# File lib/pakyow/environment.rb, line 180
def load
  performing :load do
    if File.exist?(config.loader_path + ".rb")
      require config.loader_path
    else
      require "pakyow/integrations/bundler/setup"
      require "pakyow/integrations/bootsnap"

      require "pakyow/integrations/bundler/require"
      require "pakyow/integrations/dotenv"

      require config.environment_path

      load_apps
    end
  end
end
load_apps() click to toggle source

Loads apps located in the current project.

# File lib/pakyow/environment.rb, line 200
def load_apps
  require File.join(config.root, "config/application")
end
load_tasks() click to toggle source

@api private

# File lib/pakyow/environment.rb, line 345
def load_tasks
  require "rake"
  require "pakyow/task"

  @tasks = config.tasks.paths.uniq.each_with_object([]) do |tasks_path, tasks|
    Dir.glob(File.join(File.expand_path(tasks_path), "**/*.rake")).each do |task_path|
      tasks.concat(Pakyow::Task::Loader.new(task_path).__tasks)
    end
  end
end
logger() click to toggle source

Logger instance for the environment.

Builds and returns a default logger that's replaced in `setup`.

# File lib/pakyow/environment.rb, line 162
def logger
  @logger ||= Logger.new("dflt", output: global_logger, level: :all)
end
mount(app, at:, &block) click to toggle source

Mounts an app at a path.

The app can be any rack endpoint, but must implement an initializer like {Application#initialize}.

@param app the rack endpoint to mount @param at [String] where the endpoint should be mounted

# File lib/pakyow/environment.rb, line 174
def mount(app, at:, &block)
  mounts << { app: app, block: block, path: at }
end
register_framework(framework_name, framework_module) click to toggle source
# File lib/pakyow/environment.rb, line 285
def register_framework(framework_name, framework_module)
  @frameworks[framework_name] = framework_module
end
setup(env: nil) click to toggle source

Prepares the environment for booting.

@param env [Symbol] the environment to prepare for

# File lib/pakyow/environment.rb, line 208
def setup(env: nil)
  @env = (env ||= config.default_env).to_sym

  load

  performing :configure do
    configure!(env)
    $LOAD_PATH.unshift(config.lib)
  end

  performing :setup do
    destinations = Logger::Multiplexed.new(
      *config.logger.destinations.map { |destination, io|
        io.sync = config.logger.sync
        Logger::Destination.new(destination, io)
      }
    )

    @global_logger = config.logger.formatter.new(destinations)

    @logger = Logger::ThreadLocal.new(
      Logger.new("pkyw", output: @global_logger, level: config.logger.level)
    )

    Console.logger = Logger.new("asnc", output: @global_logger, level: :warn)
  end

  self
rescue => error
  @setup_error = error; self
end

Private Class Methods

ensure_setup_succeeded() click to toggle source
# File lib/pakyow/environment.rb, line 367
def ensure_setup_succeeded
  if @setup_error
    handle_boot_failure(@setup_error)
  end
end
handle_boot_failure(error) click to toggle source
# File lib/pakyow/environment.rb, line 373
def handle_boot_failure(error)
  @error = error

  logger.houston(error)

  if config.exit_on_boot_failure
    exit(false)
  end
end