implements 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.githooks/pre-commit/run-ruby-appraiser +17 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +173 -0
- data/Rakefile +8 -0
- data/implements.gemspec +37 -0
- data/lib/implements.rb +4 -0
- data/lib/implements/global.rb +15 -0
- data/lib/implements/implementation.rb +62 -0
- data/lib/implements/implementation/registry.rb +50 -0
- data/lib/implements/implementation/registry/element.rb +64 -0
- data/lib/implements/implementation/registry/finder.rb +42 -0
- data/lib/implements/interface.rb +58 -0
- data/lib/implements/version.rb +8 -0
- data/spec/implements_spec.rb +160 -0
- data/spec/spec_helper.rb +2 -0
- metadata +171 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
echo -e "\033[0;36mRuby Appraiser: running\033[0m"
|
3
|
+
bundle exec ruby-appraiser --all --mode=staged
|
4
|
+
result_code=$?
|
5
|
+
if [ $result_code -gt "0" ]; then
|
6
|
+
echo -en "\033[0;31m" # RED
|
7
|
+
echo "[✘] Ruby Appraiser found newly-created defects and "
|
8
|
+
echo " has blocked your commit."
|
9
|
+
echo " Fix the defects and commit again."
|
10
|
+
echo " To bypass, commit again with --no-verify."
|
11
|
+
echo -en "\033[0m" # RESET
|
12
|
+
exit $result_code
|
13
|
+
else
|
14
|
+
echo -en "\033[0;32m" # GREEN
|
15
|
+
echo "[✔] Ruby Appraiser ok"
|
16
|
+
echo -en "\033[0m" #RESET
|
17
|
+
fi
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Ryan Biesemeyer
|
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,173 @@
|
|
1
|
+
# Implements
|
2
|
+
|
3
|
+
`Implements` is a tool for building modular libraries and tools as
|
4
|
+
interfaces, for implementing those interfaces, and ensuring that
|
5
|
+
consumers are able to load the best available implementation at
|
6
|
+
runtime.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'implements'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install implements
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
`Implements` was created as a dependency of my [redis-copy][] gem, which
|
25
|
+
provides multiple implementations of each of multiple interfaces in order to
|
26
|
+
provide support for new features in redis, while falling back gracefully
|
27
|
+
(sometimes multiple steps) to less-optimal implementations when
|
28
|
+
the underlying support is not present.
|
29
|
+
|
30
|
+
[redis-copy]: https://github.com/yaauie/redis-copy
|
31
|
+
|
32
|
+
The goal of the `implements` gem in particular is to provide an implementation
|
33
|
+
registry that is attached to the interface, and can be used to provide
|
34
|
+
the best-possible implementation for a given scenario. It also allows
|
35
|
+
third-party libraries to provide their own implementations of an interface
|
36
|
+
without having to touch the library that contains their upstream interface.
|
37
|
+
|
38
|
+
Below you will find a simplified example:
|
39
|
+
|
40
|
+
``` ruby
|
41
|
+
require 'implements/global'
|
42
|
+
|
43
|
+
module RedisCopy
|
44
|
+
module KeyEmitter
|
45
|
+
extend Implements::Interface
|
46
|
+
|
47
|
+
# @param redis_connection [Object]
|
48
|
+
# @return [void]
|
49
|
+
def intialize(redis_connection)
|
50
|
+
@redis = redis_connection
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param keys [String] - ('*') a glob-ish pattern
|
54
|
+
# @return [Enumerable<String>]
|
55
|
+
def keys(pattern = '*')
|
56
|
+
raise NotImplementedError
|
57
|
+
end
|
58
|
+
|
59
|
+
# ...
|
60
|
+
|
61
|
+
class Default
|
62
|
+
implements KeyEmitter
|
63
|
+
|
64
|
+
def keys(pattern = '*')
|
65
|
+
@redis.keys(pattern)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Scanner
|
70
|
+
# note how a block is given to `implements`.
|
71
|
+
# this block is called with the class' initialize arguments
|
72
|
+
# to determine whether or not this implementation is compatible
|
73
|
+
# with the input and its state before initializing the object.
|
74
|
+
implements KeyEmitter do |redis_connection|
|
75
|
+
bin_version = Gem::Version.new(redis_connection.info['redis_version'])
|
76
|
+
bin_requirement = Gem::Requirement.new('>= 2.7.105')
|
77
|
+
|
78
|
+
break false unless bin_requirement.satisfied_by?(bin_version)
|
79
|
+
|
80
|
+
redis_connection.respond_to?(:scan_each)
|
81
|
+
end
|
82
|
+
|
83
|
+
def keys(pattern = '*')
|
84
|
+
@redis.scan_each(match: pattern, count: 1000)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
The consumer of this interface, then, can get the best available implementation,
|
92
|
+
given their environment and the object(s) passed to `#initialize`, without
|
93
|
+
having to know anything about the implementations themselves:
|
94
|
+
|
95
|
+
``` ruby
|
96
|
+
source_redis = Redis.new(port: 9736) # a scanner-compatible redis process (>= 2.7.105)
|
97
|
+
key_emitter = RedisCopy::KeyEmitter.implementation.new(source_redis)
|
98
|
+
# => <RedisCopy::KeyEmitter::Scanner: ... >
|
99
|
+
key_emitter.keys('schedule:*').to_enum
|
100
|
+
# => <Enumerator ...>
|
101
|
+
|
102
|
+
source_redis = Redis.new(port: 9737) # a scanner-incompatible redis process (< 2.7.105)
|
103
|
+
key_emitter = RedisCopy::KeyEmitter.implementation.new(source_redis)
|
104
|
+
# => <RedisCopy::KeyEmitter::Default: ... >
|
105
|
+
key_emitter.keys('schedule:*').to_enum
|
106
|
+
# => <Enumerator ...>
|
107
|
+
```
|
108
|
+
|
109
|
+
The consumer can choose to favor a particular implementation by name:
|
110
|
+
|
111
|
+
``` ruby
|
112
|
+
key_emitter = RedisCopy::KeyEmitter.implementation(:scanner).new(source_redis)
|
113
|
+
# => <RedisCopy::KeyEmitter::Scanner: ... >
|
114
|
+
```
|
115
|
+
|
116
|
+
And if a compatible implementation cannot be found, an appropriate exception
|
117
|
+
is raised:
|
118
|
+
|
119
|
+
``` ruby
|
120
|
+
key_emitter = RedisCopy::KeyEmitter.implementation(:scanner).new(source_redis)
|
121
|
+
# Implements::implementation::NotFound: no compatible implementation for RedisCopy::KeyEmitter>
|
122
|
+
```
|
123
|
+
|
124
|
+
The implementation finder assumes that implementations loaded later are
|
125
|
+
somehow better than those loaded before them, but a consumer can specify
|
126
|
+
first preference and fallback groups:
|
127
|
+
|
128
|
+
``` ruby
|
129
|
+
key_emitter = RedisCopy::KeyEmitter.implementation(:scanner, :auto).new(source_redis)
|
130
|
+
# => <RedisCopy::KeyEmitter::Default: ... >
|
131
|
+
```
|
132
|
+
|
133
|
+
And implementations can be added which are not in the auto load-order and have
|
134
|
+
to be explicitly asked for:
|
135
|
+
|
136
|
+
``` ruby
|
137
|
+
# Like this insane whack-a-mole implementation,
|
138
|
+
# Which we wouldn't want anyone to accidentally use:
|
139
|
+
class RedisCopy::KeyEmitter::WhackAMole
|
140
|
+
Implements RedisCopy::KeyEmitter, auto: false
|
141
|
+
|
142
|
+
def keys(pattern = '*')
|
143
|
+
return enum_for(__method__, pattern) unless block_given?
|
144
|
+
while(key = redis.randomkey)
|
145
|
+
yield key if glob_match?(pattern, key)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# ...
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
``` ruby
|
154
|
+
key_emitter = RedisCopy::KeyEmitter.implementation(:whack_a_mole).new(source_redis)
|
155
|
+
# => <RedisCopy::KeyEmitter::WhackAMole: ... >
|
156
|
+
# But it doesn't come back unless you ask for it.
|
157
|
+
key_emitter = RedisCopy::KeyEmitter.implementation.new(source_redis)
|
158
|
+
# => <RedisCopy::KeyEmitter::Scanner: ... >
|
159
|
+
```
|
160
|
+
|
161
|
+
# TODO:
|
162
|
+
|
163
|
+
- Provide tools for testing *all* implementations of an interface.
|
164
|
+
- Finalize syntax for the check. A block alone is convenient, but not clear.
|
165
|
+
- Finalize scope of check. Allocate and instance_exec? Run all as hooks before `#initialize`?
|
166
|
+
|
167
|
+
## Contributing
|
168
|
+
|
169
|
+
1. Fork it
|
170
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
171
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
172
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
173
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/implements.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'implements/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'implements'
|
9
|
+
spec.version = Implements::VERSION
|
10
|
+
spec.authors = ['Ryan Biesemeyer']
|
11
|
+
spec.email = ['ryan@simplymeasured.com']
|
12
|
+
spec.summary = 'A tool for building and implementing interfaces.'
|
13
|
+
spec.description = <<-EODESC.gsub(/^[\w]+/, ' ').squeeze
|
14
|
+
Implements is a tool for building modular libraries and tools as
|
15
|
+
interfaces, for implementing those interfaces, and ensuring that
|
16
|
+
consumers are able to load the best available implementation at
|
17
|
+
runtime.
|
18
|
+
EODESC
|
19
|
+
spec.homepage = 'https://github.com/yaauie/implements'
|
20
|
+
spec.license = 'MIT'
|
21
|
+
|
22
|
+
spec.files = `git ls-files`.split($/)
|
23
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
24
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.add_runtime_dependency 'activesupport'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'bundler', '~> 1.3', '>= 1.3.5'
|
30
|
+
spec.add_development_dependency 'rake'
|
31
|
+
spec.add_development_dependency 'rspec', '~> 2.14'
|
32
|
+
|
33
|
+
|
34
|
+
# code quality
|
35
|
+
spec.add_development_dependency 'ruby-appraiser-reek'
|
36
|
+
spec.add_development_dependency 'ruby-appraiser-rubocop'
|
37
|
+
end
|
data/lib/implements.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'implements'
|
3
|
+
|
4
|
+
# Add functionality to Class, which enables us to use
|
5
|
+
# `Implements::Implementation`'s ::implements method
|
6
|
+
# without having to pre-extend the class.
|
7
|
+
class Class
|
8
|
+
def implements(*args, &block)
|
9
|
+
return super if defined?(super)
|
10
|
+
|
11
|
+
extend(Implements::Implementation)
|
12
|
+
send(__method__, *args, &block)
|
13
|
+
end
|
14
|
+
private :implements
|
15
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'active_support/inflector'
|
4
|
+
|
5
|
+
require_relative 'implementation/registry'
|
6
|
+
|
7
|
+
module Implements
|
8
|
+
# Implementation: mix into your implementations
|
9
|
+
module Implementation
|
10
|
+
# An exception raised when an implementation cannot be found.
|
11
|
+
NotFound = Class.new(NotImplementedError)
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# @api public
|
16
|
+
# @param iface [Module(Implements::Interface)]
|
17
|
+
# @param options [Hash{Symbol => Object}] - ({}) optional options hash
|
18
|
+
# @option options [Boolean] :auto - (true) whether to include this
|
19
|
+
# implementation in the interface's default search
|
20
|
+
# @option options [String] :as - The canonical name for this
|
21
|
+
# implementation, must be unique across all implementations.
|
22
|
+
# @option options [#to_s, Array<#to_s>] :groups - one or more named tags
|
23
|
+
# for this implementation, used for matching in Interface#implementation.
|
24
|
+
#
|
25
|
+
# If given, the block will be usde to determine the compatibility of this
|
26
|
+
# interface with the arguments that would be passed to the implementation's
|
27
|
+
# #initialize method.
|
28
|
+
# @yieldparam (@see self#initialize)
|
29
|
+
# @yieldreturn [Boolean]
|
30
|
+
#
|
31
|
+
# @return [void]
|
32
|
+
# @raises [TypeError] unless iface is a Implements::Interface Module
|
33
|
+
def implements(iface, options = {}, &block)
|
34
|
+
unless iface.instance_of?(Module) && iface.kind_of?(Interface)
|
35
|
+
fail(TypeError, 'Argument must be a Implements::Interface Module')
|
36
|
+
end
|
37
|
+
|
38
|
+
params = {}
|
39
|
+
params[:name] = options.fetch(:as) if options.key?(:as)
|
40
|
+
groups = []
|
41
|
+
groups << :default unless block_given?
|
42
|
+
groups << :auto if options.fetch(:auto, true)
|
43
|
+
params[:groups] = groups
|
44
|
+
|
45
|
+
iface.register_implementation(self, params, &block)
|
46
|
+
|
47
|
+
include iface
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
def self.extended(klass)
|
52
|
+
unless klass.instance_of?(Class)
|
53
|
+
fail(TypeError, "expected Class, got #{klass.class}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @api private
|
58
|
+
def self.included(base)
|
59
|
+
base && fail(ScriptError, "#{self} supports only extend, not include.")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Implements
|
6
|
+
module Implementation
|
7
|
+
# A registry of implementations, held by an interface.
|
8
|
+
# @api private
|
9
|
+
class Registry
|
10
|
+
# @api private
|
11
|
+
# @param interface [Interface]
|
12
|
+
def initialize(interface)
|
13
|
+
@interface = interface
|
14
|
+
@elements = []
|
15
|
+
end
|
16
|
+
attr_reader :interface
|
17
|
+
|
18
|
+
# Returns an enumerator of elements matching the given selectors,
|
19
|
+
# in the order specified; this is used by {Finder#find}.
|
20
|
+
# @param selectors [#to_s, Array<#to_s>] - one or more selectors
|
21
|
+
# @return [Enumerator<Element>]
|
22
|
+
def elements(selectors)
|
23
|
+
Enumerator.new do |yielder|
|
24
|
+
yielded = Set.new
|
25
|
+
Array(selectors).product(@elements) do |selector, element|
|
26
|
+
next unless element.match?(selector)
|
27
|
+
yielder << element if yielded.add?(element)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
# @return [Array<String>]
|
34
|
+
def list_names
|
35
|
+
@elements.map(&:name).compact
|
36
|
+
end
|
37
|
+
|
38
|
+
# @api private
|
39
|
+
# @param implementation [Implementation]
|
40
|
+
# @param options [Hash{Symbol=>Object}] (see: Element#initialize)
|
41
|
+
# @param check [#call, nil]
|
42
|
+
def register(implementation, options, check)
|
43
|
+
@elements.unshift Element.new(self, implementation, options, check)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
require_relative 'registry/element'
|
50
|
+
require_relative 'registry/finder'
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Implements
|
4
|
+
# An Element in a Registry
|
5
|
+
# @api private
|
6
|
+
class Implementation::Registry::Element
|
7
|
+
# @api private
|
8
|
+
# @param registry [Implementation::Registry]
|
9
|
+
# @param implementation [Implementation]
|
10
|
+
# @param options [Hash{Symbol=>Object}]
|
11
|
+
# @param check [#call, nil]
|
12
|
+
def initialize(registry, implementation, options, check)
|
13
|
+
@registry = registry
|
14
|
+
@implementation = implementation
|
15
|
+
@options = options
|
16
|
+
@check = check
|
17
|
+
end
|
18
|
+
attr_reader :implementation
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
# @param selector [#===]
|
22
|
+
# @return [Boolean]
|
23
|
+
def match?(selector)
|
24
|
+
selector = selector.to_s if selector.kind_of?(Symbol)
|
25
|
+
selector = selector.dasherize if selector.kind_of?(String)
|
26
|
+
groups.map(&:to_s).any? { |group| selector === group }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check the implementation agains the args that would be used
|
30
|
+
# to instantiate it.
|
31
|
+
# @api private
|
32
|
+
# @params *args [Array<Object>]
|
33
|
+
# @return [Boolean]
|
34
|
+
def check?(*args)
|
35
|
+
return true unless @check
|
36
|
+
@check.call(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
# @return [String]
|
41
|
+
def name
|
42
|
+
groups.first
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
# @return [Array<String>]
|
49
|
+
def groups
|
50
|
+
@groups ||= [@options[:name],
|
51
|
+
implementation_descriptors,
|
52
|
+
@options[:groups]].flatten.compact.map(&:to_s)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @api private
|
56
|
+
# @return [Array<String>]
|
57
|
+
def implementation_descriptors
|
58
|
+
desc = []
|
59
|
+
desc << (name = @implementation.name)
|
60
|
+
desc << (name && name.sub(/^(::)?#{@registry.interface}::/, ''))
|
61
|
+
desc.compact.map(&:underscore).map(&:dasherize).reverse
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Implements
|
4
|
+
# A Finder, plumbed to a Registry.
|
5
|
+
# @api private
|
6
|
+
class Implementation::Registry::Finder
|
7
|
+
# @api private
|
8
|
+
# @param registry [Implementation::Registry]
|
9
|
+
# @param selectors [Array<#===>] Typically an array of strings
|
10
|
+
def initialize(registry, selectors)
|
11
|
+
@registry = registry
|
12
|
+
@selectors = selectors
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns an instance of the @registry.interface that supports the given
|
16
|
+
# arguments.
|
17
|
+
# @api private
|
18
|
+
def new(*args, &block)
|
19
|
+
implementation = find(*args)
|
20
|
+
implementation.new(*args, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Find a suitable implementation of the given interface,
|
24
|
+
# given the args that would be passed to its #initialize
|
25
|
+
# and our selectors
|
26
|
+
# @api private
|
27
|
+
def find(*args)
|
28
|
+
@registry.elements(@selectors).each do |config|
|
29
|
+
next unless config.check?(*args)
|
30
|
+
return config.implementation
|
31
|
+
end
|
32
|
+
|
33
|
+
fail(Implementation::NotFound,
|
34
|
+
"no compatible implementation for #{inspect}")
|
35
|
+
end
|
36
|
+
|
37
|
+
# @api private
|
38
|
+
def inspect
|
39
|
+
"<#{@registry.interface}::implementation(#{@selectors.join(', ')})>"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Implements
|
4
|
+
# Interface: mix into your interfaces.
|
5
|
+
module Interface
|
6
|
+
# Used to find a suitable implementation
|
7
|
+
# @api public
|
8
|
+
# @param [*selectors] zero or more selectors to use for finding an
|
9
|
+
# implementation of this interface. If none is given, :auto is assumed.
|
10
|
+
# @return [Implementation::Registry::Finder]
|
11
|
+
def implementation(*selectors)
|
12
|
+
selectors << :auto if selectors.empty?
|
13
|
+
Implementation::Registry::Finder.new(@implementations, selectors)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a list of implementations by resolvable name.
|
17
|
+
# @api public
|
18
|
+
# @return [Array<String>]
|
19
|
+
def list_implementation_names
|
20
|
+
@implementations.list_names.map(&:to_s).uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
# Find an instantiate a suitable implementation on auto mode
|
24
|
+
# @see Implementation::Registry::Find#new
|
25
|
+
# @api public
|
26
|
+
def new(*args, &block)
|
27
|
+
implementation(:auto).new(*args, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
# Used by Implementation#implements
|
32
|
+
# @param implements (see Registry#register)
|
33
|
+
# @param options (see Registry#register)
|
34
|
+
# @param &block (see Registry#register)
|
35
|
+
# @return (see Registry#register)
|
36
|
+
def register_implementation(implementation, options, &block)
|
37
|
+
@implementations.register(implementation, options, block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Bad things happen when used improperly. Make it harder to get it wrong.
|
41
|
+
# @api private
|
42
|
+
def self.included(base)
|
43
|
+
base && fail(ScriptError, "#{self} supports only extend, not include.")
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set up the interface.
|
47
|
+
# @param base [Module]
|
48
|
+
# @api private
|
49
|
+
def self.extended(base)
|
50
|
+
unless base.instance_of?(Module)
|
51
|
+
fail(TypeError, "expected Module, got #{base.class}")
|
52
|
+
end
|
53
|
+
|
54
|
+
base.instance_variable_set(:@implementations,
|
55
|
+
Implementation::Registry.new(base))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative 'spec_helper'
|
3
|
+
|
4
|
+
describe Implements do
|
5
|
+
context 'On a Widget interface' do
|
6
|
+
let!(:interface) do
|
7
|
+
interface = Module.new do
|
8
|
+
extend Implements::Interface
|
9
|
+
|
10
|
+
def initialize(number)
|
11
|
+
@number = number
|
12
|
+
end
|
13
|
+
|
14
|
+
def wobble() :interface end
|
15
|
+
end
|
16
|
+
|
17
|
+
stub_const('::Widget', interface)
|
18
|
+
def Widget.inspect() 'Widget' end
|
19
|
+
|
20
|
+
interface
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'with two conditional & one default implementations' do
|
24
|
+
# order and registry matters, so define the implementations
|
25
|
+
# with let! to ensure they are run and memoized before the
|
26
|
+
# example is run.
|
27
|
+
let!(:default_implementation) do
|
28
|
+
Class.new do
|
29
|
+
extend(Implements::Implementation)
|
30
|
+
implements ::Widget
|
31
|
+
end
|
32
|
+
end
|
33
|
+
let!(:small_implementation) do
|
34
|
+
Class.new do
|
35
|
+
extend(Implements::Implementation)
|
36
|
+
implements ::Widget, as: :small do |number|
|
37
|
+
number < 10
|
38
|
+
end
|
39
|
+
def wobble() :small end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
let!(:large_implementation) do
|
43
|
+
Class.new do
|
44
|
+
extend(Implements::Implementation)
|
45
|
+
implements ::Widget, as: :large do |number|
|
46
|
+
number >= 1_000_000
|
47
|
+
end
|
48
|
+
def wobble() :large end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
let!(:odd_implementation) do
|
52
|
+
Class.new do
|
53
|
+
extend(Implements::Implementation)
|
54
|
+
implements ::Widget, as: :odd do |number|
|
55
|
+
number.odd?
|
56
|
+
end
|
57
|
+
def wobble() :odd end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'Interface#new' do
|
62
|
+
let(:result) { interface.new(input) }
|
63
|
+
subject { result }
|
64
|
+
|
65
|
+
context 'matching the large implementation' do
|
66
|
+
let(:input) { 10_000_000 }
|
67
|
+
it 'should return the large implementation' do
|
68
|
+
expect(result).to be_an_instance_of large_implementation
|
69
|
+
end
|
70
|
+
it { should be_a Widget }
|
71
|
+
its(:wobble) { should be :large } # ensure proper inheritance
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'matching the small implementation' do
|
75
|
+
let(:input) { 2 }
|
76
|
+
it 'should return the small implementation' do
|
77
|
+
expect(result).to be_an_instance_of small_implementation
|
78
|
+
end
|
79
|
+
it { should be_a Widget }
|
80
|
+
its(:wobble) { should be :small } # ensure proper inheritance
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'matching neither small nor large' do
|
84
|
+
let(:input) { 1_000 }
|
85
|
+
it 'should return the default implementation' do
|
86
|
+
expect(result).to be_an_instance_of default_implementation
|
87
|
+
end
|
88
|
+
it { should be_a Widget }
|
89
|
+
its(:wobble) { should be :interface } # ensure proper inheritance
|
90
|
+
end
|
91
|
+
|
92
|
+
context '#implementation.new' do
|
93
|
+
let(:result) { interface.implementation(*selectors).new(input) }
|
94
|
+
subject { result }
|
95
|
+
context 'specifying the large implementation' do
|
96
|
+
let(:selectors) { [:large] }
|
97
|
+
context 'not matching the large implementation' do
|
98
|
+
let(:input) { 2 }
|
99
|
+
it 'should raise an appropriate exception' do
|
100
|
+
expect do
|
101
|
+
result
|
102
|
+
end.to raise_error Implements::Implementation::NotFound
|
103
|
+
end
|
104
|
+
end
|
105
|
+
context 'matching the large implementation' do
|
106
|
+
let(:input) { 10_000_000 }
|
107
|
+
it 'should return the large implementation' do
|
108
|
+
expect(result).to be_an_instance_of large_implementation
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
context 'specifying the small implementation' do
|
113
|
+
let(:selectors) { [:small] }
|
114
|
+
context 'not matching the small implementation' do
|
115
|
+
let(:input) { 1_000_000 }
|
116
|
+
it 'should raise an appropriate exception' do
|
117
|
+
expect do
|
118
|
+
result
|
119
|
+
end.to raise_error Implements::Implementation::NotFound
|
120
|
+
end
|
121
|
+
end
|
122
|
+
context 'matching the small implementation' do
|
123
|
+
let(:input) { 2 }
|
124
|
+
it 'should return the small implementation' do
|
125
|
+
expect(result).to be_an_instance_of small_implementation
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
context 'when multiple implementations match' do
|
130
|
+
let(:input) { 7 }
|
131
|
+
context 'specifying the small or odd implementation' do
|
132
|
+
let(:selectors) { [:small, :odd] }
|
133
|
+
it 'should favor small' do
|
134
|
+
expect(result).to be_an_instance_of small_implementation
|
135
|
+
end
|
136
|
+
end
|
137
|
+
context 'specifying the odd or small implementation' do
|
138
|
+
let(:selectors) { [:odd, :small] }
|
139
|
+
it 'should favor odd' do
|
140
|
+
expect(result).to be_an_instance_of odd_implementation
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'with no implementations' do
|
149
|
+
context 'Interface#new' do
|
150
|
+
let(:result) { interface.new(input) }
|
151
|
+
subject { result }
|
152
|
+
|
153
|
+
let(:input) { 1_000 }
|
154
|
+
it 'should raise an appropriate exception' do
|
155
|
+
expect { result }.to raise_error Implements::Implementation::NotFound
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: implements
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ryan Biesemeyer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-11-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.3'
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.3.5
|
41
|
+
type: :development
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ~>
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '1.3'
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 1.3.5
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: rake
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
type: :development
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rspec
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.14'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ~>
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '2.14'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: ruby-appraiser-reek
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
- !ruby/object:Gem::Dependency
|
101
|
+
name: ruby-appraiser-rubocop
|
102
|
+
requirement: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
type: :development
|
109
|
+
prerelease: false
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
description: ! " Implements is a tol for building modular libraries and tols as\n
|
117
|
+
interfaces, for implementing those interfaces, and ensuring that\n consumers are
|
118
|
+
able to load the best available implementation at\n runtime.\n"
|
119
|
+
email:
|
120
|
+
- ryan@simplymeasured.com
|
121
|
+
executables: []
|
122
|
+
extensions: []
|
123
|
+
extra_rdoc_files: []
|
124
|
+
files:
|
125
|
+
- .githooks/pre-commit/run-ruby-appraiser
|
126
|
+
- .gitignore
|
127
|
+
- .travis.yml
|
128
|
+
- Gemfile
|
129
|
+
- LICENSE.txt
|
130
|
+
- README.md
|
131
|
+
- Rakefile
|
132
|
+
- implements.gemspec
|
133
|
+
- lib/implements.rb
|
134
|
+
- lib/implements/global.rb
|
135
|
+
- lib/implements/implementation.rb
|
136
|
+
- lib/implements/implementation/registry.rb
|
137
|
+
- lib/implements/implementation/registry/element.rb
|
138
|
+
- lib/implements/implementation/registry/finder.rb
|
139
|
+
- lib/implements/interface.rb
|
140
|
+
- lib/implements/version.rb
|
141
|
+
- spec/implements_spec.rb
|
142
|
+
- spec/spec_helper.rb
|
143
|
+
homepage: https://github.com/yaauie/implements
|
144
|
+
licenses:
|
145
|
+
- MIT
|
146
|
+
post_install_message:
|
147
|
+
rdoc_options: []
|
148
|
+
require_paths:
|
149
|
+
- lib
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
151
|
+
none: false
|
152
|
+
requirements:
|
153
|
+
- - ! '>='
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: '0'
|
156
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
157
|
+
none: false
|
158
|
+
requirements:
|
159
|
+
- - ! '>='
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
requirements: []
|
163
|
+
rubyforge_project:
|
164
|
+
rubygems_version: 1.8.24
|
165
|
+
signing_key:
|
166
|
+
specification_version: 3
|
167
|
+
summary: A tool for building and implementing interfaces.
|
168
|
+
test_files:
|
169
|
+
- spec/implements_spec.rb
|
170
|
+
- spec/spec_helper.rb
|
171
|
+
has_rdoc:
|