houston/houston-core

View on GitHub
lib/houston/boot/configuration.rb

Summary

Maintainability
C
1 day
Test Coverage
root = File.expand_path(File.join(File.dirname(__FILE__), "../../.."))
require File.join(root, "lib/hash_dsl")
require File.join(root, "lib/core_ext/hash")
require File.join(root, "lib/core_ext/kernel")
require File.join(root, "lib/core_ext/exception")
require File.join(root, "lib/houston/boot/triggers")
require File.join(root, "lib/houston/boot/observer")
require File.join(root, "lib/houston/boot/actions")
require File.join(root, "lib/houston/boot/timer")
require File.join(root, "lib/houston/boot/provider")
require File.join(root, "lib/houston/adapters")

module Houston
module_function
  def deprecation_notice(message, stack_offset=1)
    message = message.gsub /<b>(.*)<\/b>/, "\e[1m\\1\e[0;34m"
    puts "\e[34mDEPRECATION: #{message}\e[0;90m\n#{caller[stack_offset]}\e[0m\n\n"
  end



  class Configuration
    attr_reader :observer, :actions, :timer, :oauth_providers

    def initialize
      @root = Rails.root
      @use_ssl = Rails.env.production?
      @oauth_providers = []
      @roles = Hash.new { |hash, key| hash[key] = [] }
      @roles["Team Owner"].push(Proc.new do |team|
        can :manage, team
        can :manage, Project, team_id: team.id
      end)
      @modules = []
    end

    def triggers
      return @triggers if defined?(@triggers)
      @triggers = Houston::Triggers.new(self)
    end

    def observer
      return @observer if defined?(@observer)
      @observer = Houston::Observer.new
    end

    def actions
      return @actions if defined?(@actions)
      @actions = Houston::Actions.new
    end

    def timer
      return @timer if defined?(@timer)
      @timer = Houston::Timer.new
    end




    # Global configuration

    def root(*args)
      if args.any?
        @root = args.first

        # Keep structure.sql in instances' db directory
        ActiveRecord::Tasks::DatabaseTasks.db_dir = root.join("db")

        # Configure Houston
        Houston::Application.paths["config/database"] = root.join("config/database.yml")
        Houston::Application.paths["public"] = root.join("public")
        Houston::Application.paths["log"] = root.join("log/#{Rails.env}.log")
        Houston::Application.paths["tmp"] = root.join("tmp")
        Houston::Application.paths["config/environments"] << root.join("config/environments")

        # ActionCable sets the default path for its config file
        # later on during initialization. We need to override the
        # path just before ActionCable is initialized.
        cable_config = Houston.root.join("config/cable.yml")
        if File.exists?(cable_config)
          ActiveSupport.on_load(:action_cable) do
            Houston::Application.paths["config/cable"] = cable_config

            # Make sure that we've loaded the Instance's config file
            # c.f. https://github.com/rails/rails/blob/v5.0.0.1/actioncable/lib/action_cable/engine.rb#L31
            ActionCable.server.config.cable = Rails.application.config_for(cable_config).with_indifferent_access
          end

          # Make sure that we've loaded the Instance's config file
          # c.f. https://github.com/rails/rails/blob/v5.0.0.1/actioncable/lib/action_cable/engine.rb#L31
          ActionCable.server.config.cable = Rails.application.config_for(cable_config).with_indifferent_access
        else
          Rails.application.config.before_initialize do
            Rails.logger.warn "\e[33m[boot] \e[93m#{cable_config}\e[33m does not exist: you will not be able to use Houston.observer on the client\e[0m"
          end
        end
      end

      @root
    end

    def title(*args)
      @title = args.first if args.any?
      @title ||= "Houston"
    end

    def host(*args)
      if args.any?
        @host = args.first

        if Rails.env.production?
          Houston::Application.config.action_cable.mount_path = nil
          Houston::Application.config.action_cable.url = "wss://#{host}:4200"
          Houston::Application.config.action_cable.allowed_request_origins = %w{http https}
            .map { |protocol| "#{protocol}://#{host}" }
        end
      end
      @host ||= nil
    end

    def use_ssl(*args)
      @use_ssl = args.first if args.any?
      @use_ssl
    end

    def use_ssl?
      @use_ssl
    end

    def secret_key_base(*args)
      return Houston::Application.config.secret_key_base if args.none?
      Houston::Application.config.secret_key_base = args.first
    end

    def time_zone(*args)
      return Rails.application.config.time_zone if args.none?
      Rails.application.config.time_zone = args.first
      Time.zone = args.first
    end

    def mailer_sender(*args)
      if args.any?
        @mailer_sender = args.first
        (Rails.application.config.action_mailer.default_options ||= {}).merge!(from: @mailer_sender)
      end
      @mailer_sender ||= nil
    end

    def mailer_from
      require "mail"

      Mail::Address.new.tap do |email|
        email.display_name = title
        email.address = mailer_sender
      end.to_s
    end

    def google_analytics(&block)
      @google_analytics = HashDsl.hash_from_block(block) if block_given?
      @google_analytics ||= {}
    end

    def password_length(*args)
      @password_length = args.first if args.any?
      @password_length ||= 8..128
    end

    def parallelization(*args)
      @parallelization = args.first if args.any?
      @parallelization ||= :off
    end

    def parallelize?
      parallelization == :on
    end

    def smtp(&block)
      Rails.application.config.action_mailer.smtp_settings = HashDsl.hash_from_block(block) if block_given?
      Rails.application.config.action_mailer.smtp_settings
    end

    def s3(&block)
      @s3 = HashDsl.hash_from_block(block) if block_given?
      @s3 ||= {}
    end

    def oauth(provider_name, &block)
      settings = HashDsl.hash_from_block(block)
      provider = Houston.oauth.get_provider(provider_name)

      raise ArgumentError, "Provider must define a client_id" if settings[:client_id].blank?
      raise ArgumentError, "Provider must define a client_secret" if settings[:client_secret].blank?

      provider.client_id = settings[:client_id]
      provider.client_secret = settings[:client_secret]

      @oauth_providers << provider_name.to_s
    end

    def project_categories(*args)
      @project_categories = args if args.any?
      @project_categories ||= []
    end

    def navigation(*args)
      @navigation = args if args.any?
      return Houston.navigation.slugs unless @navigation
      @navigation & Houston.navigation.slugs
    end

    def project_features(*args)
      @project_features = args if args.any?
      return Houston.project_features.all unless @project_features
      @project_features & Houston.project_features.all
    end



    def project_colors(*args)
      new_hash = Hash.new(ColorValue.new("default", "505050"))
      @project_colors = args.first.each_with_object(new_hash) { |(key, hex), hash| hash[key] = ColorValue.new(key, hex) } if args.any?
      @project_colors ||= new_hash
    end

    def environments(*args)
      @environments = args if args.any?
      @environments ||= []
    end

    def roles
      @roles.keys
    end

    def role(role_name, &abilities_block)
      @roles[role_name].push abilities_block
    end





    # Permissions

    def abilities(&block)
      @abilities_block = block
    end

    def defines_abilities?
      @abilities_block.present?
    end

    def configure_abilities(context, user)
      context.instance_exec(user, &@abilities_block)
    end

    def configure_team_abilities(context, teammate)
      teammate.roles.each do |role|
        context.can :read, teammate.team
        @roles.fetch(role).each do |abilities_block|
          context.instance_exec(teammate.team, &abilities_block)
        end
      end
    end





    # Modules

    def use(module_name, &block)
      mod = self.module(module_name)
      mod ||= ::Houston::Module.new(module_name).tap { |mod| @modules << mod }
      if mod.accepts_configuration?
        mod.load_configuration(block)
      else raise ArgumentError, "#{module_name} does not accept configuration"
      end if block_given?
      mod.dependencies.each(&method(:use))
      mod
    end
    attr_reader :modules

    def uses?(module_name)
      module_name = module_name.to_s
      modules.any? { |mod| mod.name == module_name }
    end

    def module(module_name)
      module_name = module_name.to_s
      modules.detect { |mod| mod.name == module_name }
    end





    # Actions and Triggers

    def action(name, required_params=[], &block)
      actions.define(name, required_params, &block)
    end

    def on(*args, &block)
      event_name, action_name = extract_trigger_and_action!(args)
      event = Houston.events[event_name]

      unless event
        puts "\e[31mUnregistered event: \e[1m#{event_name}\e[0;90m\n#{caller[0]}\e[0m\n\n"
        return
      end

      action = assert_action! action_name, event.params, &block
      action.assert_required_params! event.params

      triggers.on event_name, action_name
      action
    end

    def every(*args, &block)
      interval, action_name = extract_trigger_and_action!(args)
      action = assert_action! action_name, &block
      action.assert_required_params! []
      triggers.every interval, action_name
      action
    end

    private def extract_trigger_and_action!(args)
      if args.first.is_a?(Hash)
        return args.shift.to_a[0] if args.first.one?
        raise ArgumentError, "Unrecognized trigger: #{args.inspect}"
      end
      return args.shift(2) if args.length >= 2
      raise NotImplementedError, "I haven't been programmed to extract trigger and action_name from #{args.inspect}"
    end

    private def assert_action!(name, required_params=[], &block)
      if block_given?
        action name, required_params, &block
      elsif action = actions[name]
        action
      else
        raise ArgumentError, "An action named #{name.inspect} is not defined"
      end
    end





    def load(glob)
      __file__ = caller[0].split(":")[0]
      glob << ".rb" unless glob.end_with? ".rb"
      Dir.glob("#{File.dirname(__file__)}/#{glob}").each do |file|
        next if File.directory?(file)
        require file
      end
    end



    # Validation

    def validate!
      raise MissingConfiguration, <<-ERROR unless mailer_sender

        Houston requires a default email address to be supplied for mailers
        You can set the address by adding the following line to config/config.rb:

          mailer_sender "houston@my-company.com"

        ERROR
    end

    def method_missing(name, *args, &block)
      puts "\e[31mMissing Configuration option: \e[1m#{name}\e[0;90m\n#{caller[0]}\e[0m\n\n"
      nil
    end

  end



  class Module
    attr_reader :name

    def initialize(module_name)
      @name = module_name.to_s
    end

    def accepts_configuration?
      namespace.respond_to?(:config)
    end

    def load_configuration(moduleconfig)
      namespace.config(&moduleconfig)
    end

    def dependencies
      namespace.respond_to?(:dependencies) ? namespace.dependencies : []
    end

    def engine
      namespace::Engine
    end

    def path
      "/#{name}"
    end

    def namespace
      @namespace ||= "houston/#{name}".camelize.constantize
    end
  end





  class ColorValue
    attr_reader :name
    attr_reader :hex

    def initialize(name, hex)
      @name = name
      @hex = hex
    end

    def as_json(options={})
      name
    end

    def to_s
      name
    end

    def rgb
      "rgb(#{@hex.scan(/../).map { |s| s.to_i(16) }.join(", ")})"
    end

  end





  class NotConfigured < RuntimeError
    def initialize(message = "Houston has not been configured. Please load config/config.rb before calling Houston.config")
      super
    end
  end

  class MissingConfiguration < RuntimeError; end



module_function
  def config(&block)
    @configuration ||= Configuration.new
    if block_given?
      @configuration.instance_eval(&block)
      @configuration.validate!
    end
    @configuration
  end

  def triggers
    config.triggers
  end

  def observer
    config.observer
  end

  def actions
    config.actions
  end

  def timer
    config.timer
  end

  def root
    config.root
  end

end