diff --git a/lib/rackstash/class_registry.rb b/lib/rackstash/class_registry.rb new file mode 100644 index 0000000..953f45e --- /dev/null +++ b/lib/rackstash/class_registry.rb @@ -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] a new array populated with all registered names + def names + @registry.keys + end + + # @return [HashClass>] a new `Hash` containing all registered + # names and classes + def to_h + @registry.dup + end + end +end diff --git a/spec/rackstash/class_registry_spec.rb b/spec/rackstash/class_registry_spec.rb new file mode 100644 index 0000000..71943d1 --- /dev/null +++ b/spec/rackstash/class_registry_spec.rb @@ -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