fdutey/shirinji

View on GitHub
lib/shirinji/map.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Shirinji
  class Map
    attr_reader :beans

    # Loads a map at a given location
    #
    # @param location [string] path to the map to load
    def self.load(location)
      eval(File.read(location))
    end

    def initialize(&block)
      @beans = {}

      instance_eval(&block) if block
    end

    # Merges another map at a given location
    #
    # @param location [string] the file to include - must be an absolute path
    def include_map(location)
      merge(self.class.load(location))
    end

    # Merges a map into another one
    #
    # @param map [Shirinji::Map] the map to merge into this one
    # @raise [ArgumentError] if both map contains a bean with the same bean
    def merge(map)
      map.beans.keys.each { |name| raise_if_name_already_taken!(name) }

      beans.merge!(map.beans)
    end

    # Returns a bean based on its name
    #
    # @example accessing a bean
    #   map.get(:foo)
    #   #=> <#Shirinji::Bean ....>
    #
    # @example accessing a bean that doesn't exist
    #   map.get(:bar)
    #   #=> raises ArgumentError (unknown bean)
    #
    # @param name [Symbol, String] the name of the bean you want to access to
    # @return [Bean] A bean with the given name or raises an error
    # @raise [ArgumentError] if trying to access a bean that doesn't exist
    def get(name)
      bean = beans[name.to_sym]
      raise ArgumentError, "Unknown bean #{name}" unless bean

      bean
    end

    # Add a bean to the map
    #
    # @example build a class bean
    #   map.bean(:foo, klass: 'Foo', access: :singleton)
    #
    # @example build a class bean with attributes
    #   map.bean(:foo, klass: 'Foo', access: :singleton) do
    #     attr :bar, ref: :baz
    #   end
    #
    # @example build a value bean
    #   map.bean(:bar, value: 5)
    #
    # @example build a lazy evaluated value bean
    #   map.bean(:bar, value: Proc.new { 5 })
    #
    # @param name [Symbol] the name you want to register your bean
    # @option [String] :klass the classname the bean is registering
    # @option [Object] :value the object registered by the bean
    # @option [Boolean] :construct whether the bean should be constructed or not
    # @option [Symbol] :access either :singleton or :instance.
    # @yield additional method to construct our bean
    # @raise [ArgumentError] if trying to register a bean that already exist
    def bean(name, klass: nil, access: :singleton, **others, &block)
      name = name.to_sym

      raise_if_name_already_taken!(name)

      options = others.merge(
        access: access,
        class_name: klass&.freeze
      )

      beans[name] = Bean.new(name, **options, &block)
    end

    # Scopes a given set of bean to the default options
    #
    # @example module
    #   scope(module: :Foo) do
    #     bean(:bar, klass: 'Bar')
    #   end
    #
    #   #=> bean(:bar, klass: 'Foo::Bar')
    #
    # @example prefix
    #   scope(prefix: :foo) do
    #     bean(:bar, klass: 'Bar')
    #   end
    #
    #   #=> bean(:foo_bar, klass: 'Bar')
    #
    # @example suffix
    #   scope(suffix: :bar) do
    #     bean(:foo, klass: 'Foo')
    #   end
    #
    #   #=> bean(:foo_bar, klass: 'Foo')
    #
    # @example class suffix
    #   scope(klass_suffix: :Bar) do
    #     bean(:foo, klass: 'Foo')
    #   end
    #
    #   #=> bean(:foo, klass: 'FooBar')
    #
    # It comes pretty handy when used with strongly normative naming
    #
    # @example services
    #   scope(module: :Services, klass_suffix: :Service, suffix: :service) do
    #     scope(module: :User, prefix: :user) do
    #       bean(:signup, klass: 'Signup')
    #       bean(:ban, klass: 'Ban')
    #     end
    #   end
    #
    #   #=> bean(:user_signup_service, klass: 'Services::User::SignupService')
    #   #=> bean(:user_ban_service, klass: 'Services::User::BanService')
    #
    # @param options [Hash]
    # @option options [Symbol] :module prepend module name to class name
    # @option options [Symbol] :prefix prepend prefix to bean name
    # @option options [Symbol] :suffix append suffix to bean name
    # @option options [Symbol] :klass_suffix append suffix to class name
    # @option options [Boolean] :auto_klass generates klass from name
    # @option options [Boolean] :auto_prefix generates prefix from module
    # @option options [Boolean] :construct applies `construct` on every bean
    # @yield a standard map
    def scope(**options, &block)
      Scope.new(self, **options, &block)
    end

    private

    def raise_if_name_already_taken!(name)
      return unless beans[name]

      msg = "A bean already exists with the following name: #{name}"
      raise ArgumentError, msg
    end
  end
end