justinhoward/strudel

View on GitHub
lib/strudel.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
# frozen_string_literal: true

require 'strudel/version'

# Strudel
#
# A tiny dependency injection container
class Strudel
  # Creates a new Strudel container
  #
  # @yield [self]
  def initialize
    @services = {}
    @procs = {}
    @factories = {}
    yield self if block_given?
  end

  # Get a service by key
  #
  # If the service is not set, returns `nil`.
  #
  # @param [key] key The service key
  # @return [value, nil] The service or nil if not set
  def [](key)
    return @services[key].call(self) if @factories[key]

    if @procs[key]
      @services[key] = @services[key].call(self)
      @procs.delete(key)
    end

    @services[key]
  end

  # Set a service by key and value
  #
  # If `service` is a Proc, its return value will be treated as a
  # singleton service meaning it will be initialized the first time it is
  # requested and its value will be cached for subsequent requests.
  #
  # Use the `set` method to allow using a block instead of a Proc argument.
  #
  # If `service` is not a function, its value will be stored directly.
  #
  # @param [key] key The service key
  # @param [Proc, value] service The service singleton Proc or static service
  # @return [self]
  def []=(key, service)
    set(key, service)
  end

  # Set a service by key and value
  #
  # Same as `[]=` except allows passing a block instead of a Proc argument.
  #
  # @param [key] key The service key
  # @param [Proc, value, nil] service The service singleton Proc or static
  #   service
  # @yield [self]
  # @return [self]
  def set(key, service = nil, &block)
    create(key, service || block, @procs)
  end

  # Set a factory service by key and value
  #
  # If `factory` is a function, it will be called every time the service is
  # requested. So if it returns an object, it will create a new object for
  # every request.
  #
  # If `factory` is not a function, this method acts like `set`.
  #
  # @param [key] key The service key
  # @param [Proc, block] factory The service factory Proc or static service
  # @yield [self]
  # @return [self]
  def factory(key, factory = nil, &block)
    create(key, factory || block, @factories)
  end

  # Set a protected service by name and value
  #
  # If `service` is a function, the function itself will be registered as a
  # service. So when it is requested with `get`, the function will be returned
  # instead of the function's return value.
  #
  # If `service` is not a function, this method acts like `set`.
  #
  # @param [key] key The service key
  # @param [Proc, value, nil] service The service function.
  # @yield [self]
  # @return [self]
  def protect(key, service = nil, &block)
    create(key, service || block)
  end

  # Extends an existing service and overrides it.
  #
  # The `extender` block will be called with 2 arguments: `old_value` and
  # `self`. If there is no existing `key` service, `old_value` will be nil. It
  # should return the new value for the service that will override the existing
  # one.
  #
  # If `extend` is called for a service that was created with `set`, the
  # resulting service will be a singleton.
  #
  # If `extend` is called for a service that was created with `factory`, the
  # resulting service will be a factory.
  #
  # If `extend` is called for a service that was created with `protect`, the
  # resulting service will also be protected.
  #
  # If `extender` is not a function, this method will override any existing
  # service like `set`.
  #
  # @param [key] key The service key
  # @param [Proc, value, nil] extender
  # @yield [old_value, self]
  # @return [self]
  def extend(key, extender = nil, &block)
    extender ||= block
    return set(key, extender) unless extender.is_a?(Proc) && @services.key?(key)

    extended = @services[key]
    call = @factories[key] || @procs[key]
    send(@factories[key] ? :factory : :set, key) do
      extender.call(self, call ? extended.call(self) : extended)
    end
  end

  # Iterates over the service keys
  #
  # If a block is given, the block is called with each of the service keys
  # If a block is not given, returns an `Enumerable` of the keys.
  #
  # The key order is undefined.
  #
  # @yield [key] Gives the key
  # @return [Enumerable, nil]
  def each(&block)
    return @services.each_key unless block

    @services.each_key(&block)
  end

  # Checks if a service for `key` exists.
  #
  # @return [bool] True if the service exists.
  def include?(key)
    @services.key?(key)
  end

  private

  # Create a service
  #
  # @param [key] key The service key
  # @param [value] service The service value or factory
  # @param [Hash] registry If the service is a proc, the registry
  def create(key, service, registry = nil)
    [@procs, @factories].each { |reg| reg.delete key }
    registry[key] = true if registry && service.is_a?(Proc)
    @services[key] = service
    self
  end
end