choose_class 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Joel Plane
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # choose_class
2
+
3
+ Facilitates choosing a class based on the value that would be passed to that class.
4
+ Useful for implementing the factory method pattern.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'choose_class'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install choose_class
19
+
20
+ ## Example Usage
21
+
22
+ Include ChooseClass::Base into your base class. It will gain a choose_class
23
+ method. Call choose_class and pass it a value by which to decide which class
24
+ to return.
25
+
26
+ Say we're implementing an object serializer of some sort. We may have a base
27
+ class that implments some common functionalty, and then several subclasses
28
+ that specialise the serialization for specific types.
29
+
30
+ We want to determine which class to use based on the object we are to
31
+ serialize.
32
+
33
+ ```ruby
34
+ module Serializers
35
+ class Base
36
+ include ChooseClass::Base
37
+ end
38
+
39
+ # Specialisation class for Fixnum values
40
+ class Fixnum < Base
41
+ end
42
+ end
43
+ ```
44
+
45
+ elsewhere:
46
+
47
+ ```ruby
48
+ Serializers.choose_class(1) # => Serializers::Fixnum
49
+ ```
50
+
51
+ The class chosen was Serializers::Fixnum because it is a class in the same
52
+ namespace as the base class, and it has the same name as the class of the value
53
+ passed in. (These conventions are configurable in a limited way. See spec/example)
54
+
55
+ ```ruby
56
+ Serializers.choose_class("some string") # => Serializers::Base
57
+ ```
58
+
59
+ The class chosen was Serializers::Base because there was no Serializers::String
60
+ class to choose instead.
61
+
62
+ To complete the serializer example, a final implementation might look something
63
+ like this:
64
+
65
+ ```ruby
66
+ module Serializers
67
+ class Base
68
+ include ChooseClass::Base
69
+
70
+ def self.serialize value
71
+ choose_class(value).new(value).serialize
72
+ end
73
+
74
+ def serialize
75
+ # general object serialization implementation
76
+ end
77
+ end
78
+
79
+ # Specialisation class for Fixnum values
80
+ class Fixnum < Base
81
+ def serialize
82
+ # fixnum-specific serialization implementation
83
+ end
84
+ end
85
+ end
86
+
87
+ Serializers.serialize(1) # => output of Serializers::Fixnum.serialize
88
+ Serializers.serialize("some string") # => output of Serializers::Base.serialize
89
+ ```
90
+
91
+ See also spec/example.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'choose_class/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "choose_class"
8
+ spec.version = ChooseClass::VERSION
9
+ spec.authors = ["Joel Plane"]
10
+ spec.email = ["joel.plane@gmail.com"]
11
+ spec.description = %q{Choose a class based on the value that would be passed to that class. Useful for implementing factory method pattern.}
12
+ spec.summary = %q{Choose subclass based on another objects class}
13
+ spec.homepage = "https://github.com/joelplane/choose_class"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ end
@@ -0,0 +1,9 @@
1
+ require "choose_class/version"
2
+ require "choose_class/util"
3
+ require "choose_class/class_naming_conventions"
4
+ require "choose_class/class_lookup"
5
+ require "choose_class/config"
6
+ require "choose_class/base"
7
+
8
+ module ChooseClass
9
+ end
@@ -0,0 +1,29 @@
1
+ module ChooseClass
2
+ module Base
3
+
4
+ module ClassMethods
5
+ def choose_class_config
6
+ @config ||= Config.new(self)
7
+ end
8
+
9
+ def choose_class value
10
+ class_name = class_name_for_value value
11
+ convention = choose_class_config.naming_convention
12
+ ClassLookup.new(convention).find_class_by_name class_name
13
+ end
14
+
15
+ private
16
+
17
+ # @return [String] class name
18
+ def class_name_for_value value
19
+ ChooseClass::Util.class_name_without_namespace value.class
20
+ end
21
+
22
+ end
23
+
24
+ def self.included base
25
+ base.extend ClassMethods
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module ChooseClass
2
+ class ClassLookup
3
+
4
+ # @param rules [ChooseClass::ClassNamingConventions]
5
+ def initialize rules
6
+ @rules = rules
7
+ end
8
+
9
+ # @param value_class_name [String]
10
+ def find_class_by_name value_class_name
11
+ class_name = factory_class_name value_class_name
12
+ begin
13
+ ChooseClass::Util.local_const_get(@rules.factory_namespace, class_name) ||
14
+ @rules.base_class
15
+ rescue NameError => e
16
+ if e.message =~ /#{@rules.factory_namespace.name}::#{class_name}$/
17
+ @rules.base_class
18
+ else
19
+ raise e # not the error we were expecting, so re-raise
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # @param value_class_name [String]
27
+ def factory_class_name value_class_name
28
+ @rules.factory_class_name_from_value_class_name value_class_name
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ module ChooseClass
2
+ class ClassNamingConventions
3
+
4
+ attr_reader :base_class
5
+
6
+ # @param [Class] base_class
7
+ # @param [Array] rules
8
+ def initialize base_class, rules
9
+ @base_class = base_class
10
+ @rules = rules
11
+ set_default_rules
12
+ end
13
+
14
+ def factory_namespace
15
+ rule = @rules.detect do |rule|
16
+ rule.respond_to? :factory_namespace
17
+ end
18
+ rule.factory_namespace
19
+ end
20
+
21
+ def factory_class_name_from_value_class_name class_name
22
+ @rules.each do |rule|
23
+ if rule.respond_to? :factory_class_name_from_value_class_name
24
+ class_name = rule.factory_class_name_from_value_class_name class_name
25
+ end
26
+ end
27
+ class_name
28
+ end
29
+
30
+ private
31
+
32
+ def set_default_rules
33
+ if @rules.none?{|r|r.respond_to? :factory_namespace}
34
+ @rules << ChooseClass::ClassNamingConventions::Namespace.new(@base_class, nil)
35
+ end
36
+ if @rules.none?{|r|r.respond_to? :factory_class_name_from_value_class_name}
37
+ @rules << ChooseClass::ClassNamingConventions::Suffix.new(@base_class, "")
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+ rules_dir = File.expand_path('./class_naming_conventions', File.dirname(__FILE__))
45
+ Dir[File.join(rules_dir,"*.rb")].each do |file|
46
+ require file
47
+ end
@@ -0,0 +1,21 @@
1
+ module ChooseClass
2
+ class ClassNamingConventions
3
+ class Namespace
4
+
5
+ def initialize base_class, ns
6
+ @base_class = base_class
7
+ @namespace = ns
8
+ end
9
+
10
+ # same namespace as base class
11
+ def factory_namespace
12
+ if @namespace
13
+ @namespace
14
+ else
15
+ ChooseClass::Util.module_parent @base_class
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ module ChooseClass
2
+ class ClassNamingConventions
3
+ class Suffix
4
+
5
+ def initialize base_class, suffix
6
+ @base_class = base_class
7
+ @suffix = suffix
8
+ end
9
+
10
+ def factory_class_name_from_value_class_name class_name
11
+ if @suffix
12
+ "#{class_name}#{@suffix}"
13
+ else
14
+ # TODO this path is dead code,
15
+ # but we probably want to resurrect it at some point.
16
+ "#{class_name}#{factory_class_name_suffix}"
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ # Eg.
23
+ # if base class name is "SerializerBase", returns "Serializer"
24
+ # if base class name is "Penguins", returns nil
25
+ # @return [String, nil]
26
+ def factory_class_name_suffix
27
+ match = class_name_without_namespace(@base_class).match(/(.*)Base$/)
28
+ match && match[1]
29
+ end
30
+
31
+ def class_name_without_namespace klass
32
+ ChooseClass::Util.class_name_without_namespace klass
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ module ChooseClass
2
+ class Config
3
+
4
+ def initialize base_class
5
+ @rules = []
6
+ @base_class = base_class
7
+ end
8
+
9
+ def namespace ns
10
+ @rules << ChooseClass::ClassNamingConventions::Namespace.new(@base_class, ns)
11
+ self
12
+ end
13
+
14
+ def with_suffix suffix
15
+ @rules << ChooseClass::ClassNamingConventions::Suffix.new(@base_class, suffix)
16
+ self
17
+ end
18
+
19
+ # @return [ChooseClass::ClassNamingConventions]
20
+ def naming_convention
21
+ ChooseClass::ClassNamingConventions.new @base_class, @rules
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,69 @@
1
+ module ChooseClass
2
+ module Util
3
+ class << self
4
+
5
+ # Copied from ActiveSupport Module#parent
6
+ def module_parent mod
7
+ module_parent_name(mod) ? constantize(module_parent_name(mod)) : Object
8
+ end
9
+
10
+ # Copied from ActiveSupport in Rails 3.
11
+ def constantize camel_cased_word
12
+ names = camel_cased_word.split('::')
13
+ names.shift if names.empty? || names.first.empty?
14
+
15
+ constant = Object
16
+ names.each do |name|
17
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
18
+ end
19
+ constant
20
+ end
21
+
22
+ # Copied/adapted from ActiveSupport Module#parents
23
+ def module_parents mod
24
+ parents = []
25
+ if (parent_name = module_parent_name mod)
26
+ parts = parent_name.split('::')
27
+ until parts.empty?
28
+ parents << constantize(parts * '::')
29
+ parts.pop
30
+ end
31
+ end
32
+ parents << Object unless parents.include? Object
33
+ parents
34
+ end
35
+
36
+ # @param klass [Class]
37
+ # @return [String]
38
+ def class_name_without_namespace klass
39
+ klass.name.split('::').last
40
+ end
41
+
42
+ # If the constant is not found, const_get will search on Object
43
+ # which might find a class or module we don't want - we only want modules
44
+ # inside the namespace given.
45
+ # This method wraps const_get, throwing away the result if it is not
46
+ # within the namespace.
47
+ # @param namespace [Class, Module]
48
+ # @param symbol [Symbol]
49
+ # @return [Class, Module, nil]
50
+ def local_const_get namespace, symbol
51
+ return nil unless namespace.const_defined?(symbol)
52
+ found_class = namespace.const_get(symbol)
53
+ if module_parents(found_class).include?(namespace)
54
+ found_class
55
+ else
56
+ nil
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # Copied from ActiveSupport Module#parent_name
63
+ def module_parent_name mod
64
+ mod.name =~ /::[^:]+\Z/ ? $`.freeze : nil
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ module ChooseClass
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,15 @@
1
+ module ChooseClass
2
+ module Example1
3
+
4
+ class SerializerBase
5
+ include ChooseClass::Base
6
+ choose_class_config.namespace(Example1).with_suffix("Serializer")
7
+ end
8
+
9
+ # Specialisation class for Fixnum values
10
+ class FixnumSerializer
11
+
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module ChooseClass
2
+ module Example2
3
+
4
+ class SerializerBase
5
+ include ChooseClass::Base
6
+ choose_class_config.namespace(Example2).with_suffix("")
7
+ end
8
+
9
+ # Specialisation class for Fixnum values
10
+ # Does not have to be a subclass of SerializerBase, but is in this case.
11
+ class Fixnum < SerializerBase
12
+
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module ChooseClass
2
+ module Example3
3
+
4
+ module Serializers
5
+ class Base
6
+ include ChooseClass::Base
7
+ # not calling choose_class_config in this example
8
+ end
9
+
10
+ # Specialisation class for Fixnum values
11
+ class Fixnum
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+ require File.expand_path('../../lib/choose_class', File.dirname(__FILE__))
3
+ require File.expand_path('./example1', File.dirname(__FILE__))
4
+ require File.expand_path('./example2', File.dirname(__FILE__))
5
+ require File.expand_path('./example3', File.dirname(__FILE__))
6
+
7
+ module ChooseClass
8
+ describe "serializers example 1" do
9
+ describe ".choose_class" do
10
+
11
+ it "returns a specialised instance" do
12
+ Example1::SerializerBase.choose_class(1).new.should be_a Example1::FixnumSerializer
13
+ end
14
+
15
+ it "returns the generic instance when no specific class available" do
16
+ Example1::SerializerBase.choose_class("string").new.should be_a Example1::SerializerBase
17
+ end
18
+
19
+ end
20
+ end
21
+
22
+ describe "serializers example 2" do
23
+ describe ".choose_class" do
24
+
25
+ it "returns a specialised instance" do
26
+ Example2::SerializerBase.choose_class(1).new.should be_a Example2::Fixnum
27
+ end
28
+
29
+ it "returns the generic instance when no specific class available" do
30
+ Example2::SerializerBase.choose_class("string").new.should be_a Example2::SerializerBase
31
+ end
32
+
33
+ end
34
+ end
35
+
36
+ describe "serializers example 3" do
37
+ describe ".choose_class" do
38
+
39
+ it "returns a specialised instance" do
40
+ Example3::Serializers::Base.choose_class(1).new.should be_a Example3::Serializers::Fixnum
41
+ end
42
+
43
+ it "returns the generic instance when no specific class available" do
44
+ Example3::Serializers::Base.choose_class("string").new.should be_a Example3::Serializers::Base
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,6 @@
1
+ RSpec.configure do |config|
2
+ config.treat_symbols_as_metadata_keys_with_true_values = true
3
+ config.run_all_when_everything_filtered = true
4
+ config.filter_run :focus
5
+ config.order = 'random'
6
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: choose_class
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Joel Plane
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-07-31 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: bundler
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 9
29
+ segments:
30
+ - 1
31
+ - 3
32
+ version: "1.3"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rake
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ description: Choose a class based on the value that would be passed to that class. Useful for implementing factory method pattern.
64
+ email:
65
+ - joel.plane@gmail.com
66
+ executables: []
67
+
68
+ extensions: []
69
+
70
+ extra_rdoc_files: []
71
+
72
+ files:
73
+ - .gitignore
74
+ - .rspec
75
+ - Gemfile
76
+ - LICENSE.txt
77
+ - README.md
78
+ - Rakefile
79
+ - choose_class.gemspec
80
+ - lib/choose_class.rb
81
+ - lib/choose_class/base.rb
82
+ - lib/choose_class/class_lookup.rb
83
+ - lib/choose_class/class_naming_conventions.rb
84
+ - lib/choose_class/class_naming_conventions/namespace.rb
85
+ - lib/choose_class/class_naming_conventions/suffix.rb
86
+ - lib/choose_class/config.rb
87
+ - lib/choose_class/util.rb
88
+ - lib/choose_class/version.rb
89
+ - spec/example/example1.rb
90
+ - spec/example/example2.rb
91
+ - spec/example/example3.rb
92
+ - spec/example/example_spec.rb
93
+ - spec/spec_helper.rb
94
+ homepage: https://github.com/joelplane/choose_class
95
+ licenses:
96
+ - MIT
97
+ post_install_message:
98
+ rdoc_options: []
99
+
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ hash: 3
108
+ segments:
109
+ - 0
110
+ version: "0"
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ hash: 3
117
+ segments:
118
+ - 0
119
+ version: "0"
120
+ requirements: []
121
+
122
+ rubyforge_project:
123
+ rubygems_version: 1.7.2
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: Choose subclass based on another objects class
127
+ test_files:
128
+ - spec/example/example1.rb
129
+ - spec/example/example2.rb
130
+ - spec/example/example3.rb
131
+ - spec/example/example_spec.rb
132
+ - spec/spec_helper.rb