1
0
mirror of https://github.com/meineerde/rackstash.git synced 2026-01-31 17:27:13 +00:00

Add Rackstash::ClassRegistry to register filters and encoders

Using the ClassRegistry, we can register short names for filter and
encoder classes so that they can be specified in a shorter and more
readable way during creation of the logger.
This commit is contained in:
Holger Just 2017-12-20 23:06:22 +01:00
parent 1b2dd09000
commit 01593a9b8b
2 changed files with 267 additions and 0 deletions

View File

@ -0,0 +1,114 @@
# frozen_string_literal: true
#
# Copyright 2017 Holger Just
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE.txt file for details.
module Rackstash
class ClassRegistry
include ::Enumerable
# @return [String] the human-readable singular name of the registered
# objects. It is used to build more useful error messages.
attr_reader :object_type
# @param object_type [#to_s] the human-readable singular name of the
# registered objects. It is used to build more useful error messages.
def initialize(object_type = 'class')
@object_type = object_type.to_s
@registry = {}
end
# Retrieve the registered class for a given name. If the argument is already
# a class, we return it unchanged.
#
# @param spec [Class,String,Symbol] Either a class (in which case it is
# returned directly) or the name of a registered class.
# @raise [KeyError] when giving a `String` or `Symbol` but no registered
# class was found for it
# @raise [TypeError] when giving an invalid object
# @return [Class] the registered class (when giving a `String` or `Symbol`)
# or the given class (when giving a `Class`)
def [](spec)
case spec
when Class
spec
when String, Symbol, ->(s) { s.respond_to?(:to_sym) }
@registry.fetch(spec.to_sym) do
raise KeyError, "No #{@object_type} was registered for #{spec.inspect}"
end
else
raise TypeError, "#{spec.inspect} can not be used to describe #{@object_type}s"
end
end
# Register a class for the given name.
#
# @param name [String, Symbol] the name at which the class should be
# registered
# @param registered_class [Class] the class to register at `name`
# @raise [TypeError] if `name` is not a `String` or `Symbol`, or if
# `registered_class` is not a `Class`
# @return [Class] the `registered_class`
def []=(name, registered_class)
unless registered_class.is_a?(Class)
raise TypeError, 'Can only register class objects'
end
case name
when String, Symbol
@registry[name.to_sym] = registered_class
else
raise TypeError, "Can not use #{name.inspect} to register a #{@object_type} class"
end
registered_class
end
# Remove all registered classes
#
# @return [self]
def clear
@registry.clear
self
end
# Calls the given block once for each name in `self`, passing the name and
# the registered class as parameters.
#
# An `Enumerator` is returned if no block is given.
#
# @yield [name, registered_class] calls the given block once for each name
# @yieldparam name [Symbol] the name of the registered class
# @yieldparam registered_class [Class] the registered class
# @return [Enumerator, self] `self` if a block was given or an `Enumerator`
# if no block was given.
def each
return enum_for(__method__) unless block_given?
@registry.each_pair do |name, registered_class|
yield name, registered_class
end
self
end
# Prevents further modifications to `self`. A `RuntimeError` will be raised
# if modification is attempted. There is no way to unfreeze a frozen object.
#
# @return [self]
def freeze
@registry.freeze
super
end
# @return [::Array<Symbol>] a new array populated with all registered names
def names
@registry.keys
end
# @return [Hash<Symbol=>Class>] a new `Hash` containing all registered
# names and classes
def to_h
@registry.dup
end
end
end

View File

@ -0,0 +1,153 @@
# frozen_string_literal: true
#
# Copyright 2017 Holger Just
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE.txt file for details.
require 'spec_helper'
require 'rackstash/class_registry'
describe Rackstash::ClassRegistry do
let(:registry) { described_class.new('value') }
let(:klass) { Class.new}
describe '#initialize' do
it 'sets the object_type' do
expect(registry.object_type).to eql 'value'
end
end
describe '#[]' do
before do
registry[:class] = klass
end
it 'returns the class for a String' do
expect(registry['class']).to equal klass
end
it 'returns the class for a Symbol' do
expect(registry[:class]).to equal klass
end
it 'returns an actual class' do
expect(registry[klass]).to equal klass
expect(registry[String]).to equal String
end
it 'raises a KeyError on unknown names' do
expect { registry[:unknown] }
.to raise_error(KeyError, 'No value was registered for :unknown')
expect { registry['invalid'] }
.to raise_error(KeyError, 'No value was registered for "invalid"')
end
it 'raises a TypeError on invalid names' do
expect { registry[0] }
.to raise_error(TypeError, '0 can not be used to describe values')
expect { registry[nil] }
.to raise_error(TypeError, 'nil can not be used to describe values')
expect { registry[true] }
.to raise_error(TypeError, 'true can not be used to describe values')
end
end
describe '[]=' do
it 'registers a class at the given name' do
registry[:name] = klass
registry[:alias] = klass
expect(registry[:name]).to equal klass
expect(registry[:alias]).to equal klass
end
it 'rejects invalid names' do
expect { registry[0] = Class.new }
.to raise_error(TypeError, 'Can not use 0 to register a value class')
expect { registry[nil] = Class.new }
.to raise_error(TypeError, 'Can not use nil to register a value class')
expect { registry[String] = Class.new }
.to raise_error(TypeError, 'Can not use String to register a value class')
end
it 'rejects invalid values' do
expect { registry[:foo] = 123 }
.to raise_error(TypeError, 'Can only register class objects')
expect { registry[:foo] = -> { :foo } }
.to raise_error(TypeError, 'Can only register class objects')
expect { registry[:nil] = nil }
.to raise_error(TypeError, 'Can only register class objects')
end
end
describe '#clear' do
it 'removes all registrations' do
registry[:class] = klass
expect(registry[:class]).to equal klass
expect(registry.clear).to equal registry
expect { registry[:class] }.to raise_error(KeyError)
end
end
describe '#each' do
it 'yield each registered pait' do
registry['name'] = klass
registry[:alias] = klass
expect { |b| registry.each(&b) }
.to yield_successive_args([:name, klass], [:alias, klass])
end
it 'returns the registry if a block was provided' do
registry['name'] = klass
expect(registry.each {}).to equal registry
end
it 'returns an Enumerator if no block was provided' do
registry['name'] = klass
expect(registry.each).to be_instance_of Enumerator
end
end
describe '#freeze' do
it 'freezes the object' do
expect(registry.freeze).to equal registry
expect(registry).to be_frozen
end
it 'denies all further changes' do
registry.freeze
expect { registry[:name] = klass }.to raise_error(RuntimeError)
end
end
describe '#names' do
it 'returns all registered names' do
registry['name'] = klass
registry[:alias] = klass
expect(registry.names).to eql [:name, :alias]
end
end
describe '#to_h' do
it 'returns a Hash containing all registrations' do
registry['name'] = klass
registry[:alias] = klass
expect(registry.to_h).to eql(name: klass, alias: klass)
end
it 'returns a copy of the internal data' do
registry['name'] = klass
hash = registry.to_h
hash[:alias] = klass
expect { registry[:alias] }.to raise_error(KeyError)
end
end
end