receptacle 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ require 'bundler/inline'
4
+
5
+ gemfile true do
6
+ source 'https://rubygems.org'
7
+ gem 'receptacle', '../'
8
+ gem 'mongo'
9
+ end
10
+
11
+ User = Struct.new(:id, :name)
12
+
13
+ # define our Repository
14
+ module Repository
15
+ module User
16
+ include Receptacle::Repo
17
+ mediate :find
18
+ mediate :create
19
+ end
20
+ end
21
+
22
+ # we should have a global mongo connection which can be easily reused
23
+ module Connection
24
+ class Mongo
25
+ include Singleton
26
+
27
+ def initialize
28
+ @client = ::Mongo::Client.new
29
+ end
30
+ attr_reader :client
31
+ def self.client
32
+ instance.client
33
+ end
34
+ end
35
+ end
36
+
37
+ # some strategies
38
+ module Repository
39
+ module User
40
+ module Strategy
41
+ class Mongo
42
+ def find(id:)
43
+ mongo_to_model(collection.find(_id: id))
44
+ rescue
45
+ nil
46
+ end
47
+
48
+ def create(name:)
49
+ ret = collection.insert_one(name: name)
50
+ find(id: ret['_id']) # TODO: check this
51
+ end
52
+
53
+ private
54
+
55
+ def mongo_to_model(doc)
56
+ ::User.new(doc['_id'], doc['name'])
57
+ end
58
+
59
+ def collection
60
+ Connection::Mongo.client[:users]
61
+ end
62
+ end
63
+
64
+ # in memory is using a simple class instance variable as internal storage
65
+
66
+ class InMemory
67
+ class << self; attr_accessor :store end
68
+ @store = {}
69
+ def find(id:)
70
+ store[id]
71
+ end
72
+
73
+ def create(name:)
74
+ id = BSON::ObjectId.new
75
+ store[id] = User.new(id, name)
76
+ end
77
+
78
+ private
79
+
80
+ def store
81
+ self.class.store
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # configure the repository and use it
89
+ Repository::User.strategy Repository::User::Strategy::InMemory
90
+
91
+ user = Repository::User.create(name: 'foo')
92
+ p user
93
+ p Repository::User.find(id: user.id)
94
+
95
+ # switching to mongo and we see it's using a different store but keeps the same interface
96
+ Repository::User.strategy Repository::User::Strategy::Mongo
97
+
98
+ p Repository::User.find(id: user.id)
99
+ #-> nil
100
+
101
+ user = Repository::User.create(name: 'foo')
102
+ p user
103
+ p Repository::User.find(id: user.id)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module Receptacle
3
+ module Errors
4
+ class NotConfigured < StandardError
5
+ attr_reader :repo
6
+ def initialize(repo:)
7
+ @repo = repo
8
+ super("Missing Configuration for repository: <#{repo}>")
9
+ end
10
+ end
11
+ class ReservedMethodName < StandardError; end
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ require 'receptacle/registration'
3
+ require 'receptacle/errors'
4
+
5
+ module Receptacle
6
+ module InterfaceMethods
7
+ RESERVED_METHOD_NAMES = Set.new(%i(wrappers mediate strategy delegate_to_strategy))
8
+ private_constant :RESERVED_METHOD_NAMES
9
+
10
+ # registers a method_name for the to be mediated or forwarded to the configured strategy
11
+ #
12
+ # @param method_name [String] name of method to register
13
+ def mediate(method_name)
14
+ raise Errors::ReservedMethodName if RESERVED_METHOD_NAMES.include?(method_name)
15
+ Registration.repositories[self].methods << method_name
16
+ end
17
+ alias delegate_to_strategy mediate
18
+
19
+ # get or sets the strategy
20
+ #
21
+ # @note will set the strategy for this receptacle if passed in; will only
22
+ # return the current strategy if nil or no parameter passed include
23
+ # @param value [Class,nil]
24
+ # @return [Class] current configured strategy class
25
+ def strategy(value = nil)
26
+ if value
27
+ Registration.repositories[self].strategy = value
28
+ Registration.clear_method_cache(self)
29
+ else
30
+ Registration.repositories[self].strategy
31
+ end
32
+ end
33
+
34
+ # get or sets the wrappers
35
+ #
36
+ # @note will set the wrappers for this receptacle if passed in; will only
37
+ # return the current wrappers if nil or no parameter passed include
38
+ # @param value [Class,Array(Class),nil] wrappers
39
+ # @return [Array(Class)] current configured wrappers
40
+ def wrappers(value = nil)
41
+ if value
42
+ Registration.repositories[self].wrappers = Array(value)
43
+ Registration.clear_method_cache(self)
44
+ else
45
+ Registration.repositories[self].wrappers
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ module Receptacle
3
+ # Cache describing which strategy and wrappers need to be applied for this method
4
+ # @api private
5
+ class MethodCache
6
+ # @return [Symbol] name of the method this cache belongs to
7
+ attr_reader :method_name
8
+ # @return [Class] strategy class currently setup
9
+ attr_reader :strategy
10
+ # @return [Array(Class)] Array of wrapper classes which implement a wrapper for this method
11
+ attr_reader :wrappers
12
+ # @return [Symbol] name of the before action method
13
+ attr_reader :before_method_name
14
+ # @return [Symbol] name of the after action method
15
+ attr_reader :after_method_name
16
+
17
+ def initialize(method_name:, strategy:, before_wrappers:, after_wrappers:)
18
+ @strategy = strategy
19
+ @before_method_name = :"before_#{method_name}"
20
+ @after_method_name = :"after_#{method_name}"
21
+ @method_name = method_name.to_sym
22
+ before_wrappers ||= []
23
+ after_wrappers ||= []
24
+ @wrappers = before_wrappers | after_wrappers
25
+ @skip_before_wrappers = before_wrappers.empty?
26
+ @skip_after_wrappers = after_wrappers.empty?
27
+ end
28
+
29
+ # @return [Boolean] true if no before wrappers need to be applied for this method
30
+ def skip_before_wrappers?
31
+ @skip_before_wrappers
32
+ end
33
+
34
+ # @return [Boolean] true if no after wrappers need to be applied for this method
35
+ def skip_after_wrappers?
36
+ @skip_after_wrappers
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require 'receptacle/method_cache'
3
+ require 'receptacle/registration'
4
+ require 'receptacle/errors'
5
+
6
+ module Receptacle
7
+ # module which enables a repository to mediate methods dynamically to wrappers and strategy
8
+ # @api private
9
+ module MethodDelegation
10
+ # dynamically build mediation method on first invocation if the method is registered
11
+ def method_missing(method_name, *arguments, &block)
12
+ if Registration.repositories[self].methods.include?(method_name)
13
+ public_send(__build_method(method_name), *arguments, &block)
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def respond_to_missing?(method_name, include_private = false)
20
+ Registration.repositories[self].methods.include?(method_name) || super
21
+ end
22
+
23
+ # @param method_name [#to_sym]
24
+ # @return [void]
25
+ def __build_method(method_name)
26
+ method_cache = __build_method_call_cache(method_name)
27
+ if method_cache.wrappers.nil? || method_cache.wrappers.empty?
28
+ __define_shortcut_method(method_cache)
29
+ else
30
+ __define_full_method(method_cache)
31
+ end
32
+ end
33
+
34
+ # build method cache for given method name
35
+ # @param method_name [#to_sym]
36
+ # @return [MethodCache]
37
+ def __build_method_call_cache(method_name)
38
+ config = Registration.repositories[self]
39
+ before_method_name = :"before_#{method_name}"
40
+ after_method_name = :"after_#{method_name}"
41
+
42
+ raise Errors::NotConfigured, repo: self if config.strategy.nil?
43
+ MethodCache.new(
44
+ strategy: config.strategy,
45
+ before_wrappers: config.wrappers.select { |w| w.method_defined?(before_method_name) },
46
+ after_wrappers: config.wrappers.select { |w| w.method_defined?(after_method_name) },
47
+ method_name: method_name
48
+ )
49
+ end
50
+
51
+ # build lightweight method to mediate method calls to strategy without wrappers
52
+ # @param method_cache [MethodCache] method_cache of the method to be build
53
+ # @return [void]
54
+ def __define_shortcut_method(method_cache)
55
+ define_singleton_method(method_cache.method_name) do |*args, &inner_block|
56
+ method_cache.strategy.new.public_send(method_cache.method_name, *args, &inner_block)
57
+ end
58
+ end
59
+
60
+ # build method to mediate method calls to strategy with full wrapper support
61
+ # @param method_cache [MethodCache] method_cache of the method to be build
62
+ # @return [void]
63
+ def __define_full_method(method_cache)
64
+ define_singleton_method(method_cache.method_name) do |*args, &inner_block|
65
+ __run_wrappers(method_cache, *args) do |*call_args|
66
+ method_cache.strategy.new.public_send(method_cache.method_name, *call_args, &inner_block)
67
+ end
68
+ end
69
+ end
70
+
71
+ # runtime method to call before and after wrapper in correct order
72
+ # @param method_cache [MethodCache] method_cache for the current method
73
+ # @param input_args input parameter of the repository method call
74
+ # @return strategy method return value after all wrappers where applied
75
+ def __run_wrappers(method_cache, input_args)
76
+ wrappers = method_cache.wrappers.map(&:new)
77
+ args = if method_cache.skip_before_wrappers?
78
+ input_args
79
+ else
80
+ __run_before_wrappers(wrappers, method_cache.before_method_name, input_args)
81
+ end
82
+ ret = yield(args)
83
+ return ret if method_cache.skip_after_wrappers?
84
+ __run_after_wrappers(wrappers, method_cache.after_method_name, args, ret)
85
+ end
86
+
87
+ # runtime method to execute all before wrappers
88
+ # @param wrappers [Array] all wrapper instances to be executed
89
+ # @param method_name [Symbol] name of method to be executed on wrappers
90
+ # @param args input args of the repository method
91
+ # @return processed method args by before wrappers
92
+ def __run_before_wrappers(wrappers, method_name, args)
93
+ wrappers.each do |wrapper|
94
+ next unless wrapper.respond_to?(method_name)
95
+ args = wrapper.public_send(method_name, args)
96
+ end
97
+ args
98
+ end
99
+
100
+ # runtime method to execute all after wrappers
101
+ # @param wrappers [Array] all wrapper instances to be executed
102
+ # @param method_name [Symbol] name of method to be executed on wrappers
103
+ # @param args input args to the strategy method (after processing in before wrappers)
104
+ # @param return_value return value of strategy method
105
+ # @return processed return value by all after wrappers
106
+ def __run_after_wrappers(wrappers, method_name, args, return_value)
107
+ wrappers.reverse_each do |wrapper|
108
+ next unless wrapper.respond_to?(method_name)
109
+ return_value = wrapper.public_send(method_name, return_value, args)
110
+ end
111
+ return_value
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+ require 'set'
4
+ module Receptacle
5
+ # keeps global state of repositories, the defined strategy, set wrappers and methods to mediate
6
+ class Registration
7
+ include Singleton
8
+ Tuple = Struct.new(:strategy, :wrappers, :methods)
9
+
10
+ attr_reader :repositories
11
+
12
+ def initialize
13
+ @repositories = Hash.new do |h, k|
14
+ h[k] = Tuple.new(nil, [], Set.new)
15
+ end
16
+ end
17
+
18
+ def self.repositories
19
+ instance.repositories
20
+ end
21
+
22
+ # {clear_method_cache} removes dynamically defined methods
23
+ # this is needed to make strategy and wrappers changes inside the codebase possible
24
+ def self.clear_method_cache(receptacle)
25
+ instance.repositories[receptacle].methods.each do |method_name|
26
+ begin
27
+ receptacle.singleton_class.send(:remove_method, method_name)
28
+ rescue NameError
29
+ nil
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ module Receptacle
3
+ module TestSupport
4
+ # provides helpful method to toggle strategies during testing
5
+ #
6
+ # can be used in rspec like this
7
+ #
8
+ # require 'receptacle/test_support'y
9
+ # RSpec.configure do |c|
10
+ # c.include Receptacle::TestSupport
11
+ # end
12
+ #
13
+ # RSpec.describe(UserRepository) do
14
+ # around do |example|
15
+ # with_strategy(strategy){example.run}
16
+ # end
17
+ # let(:strategy) { UserRepository::Strategy::MySQL }
18
+ # ...
19
+ # end
20
+ #
21
+ # or similar with minitest
22
+ #
23
+ # require 'receptacle/test_support'
24
+ # class UserRepositoryTest < Minitest::Test
25
+ # def described_class
26
+ # UserRepository
27
+ # end
28
+ # def repo_exists(strategy)
29
+ # with_strategy(strategy) do
30
+ # assert described_class.exists(id: 5)
31
+ # end
32
+ # end
33
+ # def test_mysql
34
+ # repo_exists(UserRepository::Strategy::MySQL)
35
+ # end
36
+ # def test_fake
37
+ # repo_exists(UserRepository::Strategy::Fake)
38
+ # end
39
+ # end
40
+ def with_strategy(strategy, repo = described_class)
41
+ original_strategy = repo.strategy
42
+ repo.strategy strategy
43
+ yield
44
+ repo.strategy original_strategy
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Receptacle
3
+ VERSION = '0.1.0'
4
+ end
data/lib/receptacle.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ require 'receptacle/version'
3
+ require 'receptacle/interface_methods'
4
+ require 'receptacle/method_delegation'
5
+
6
+ module Receptacle
7
+ module Repo
8
+ def self.included(base)
9
+ base.extend(InterfaceMethods)
10
+ base.extend(MethodDelegation)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/inline'
3
+
4
+ gemfile false do
5
+ source 'https://rubygems.org'
6
+ gem 'benchmark-ips'
7
+ gem 'receptacle', path: './..'
8
+ end
9
+
10
+ require_relative 'speed_receptacle'
11
+
12
+ Speed.strategy(Speed::Strategy::One)
13
+ Speed.wrappers [Speed::Wrappers::W1,
14
+ Speed::Wrappers::W2,
15
+ Speed::Wrappers::W3,
16
+ Speed::Wrappers::W4,
17
+ Speed::Wrappers::W5,
18
+ Speed::Wrappers::W6]
19
+
20
+ print 'w/ wrappers'
21
+ Benchmark.ips do |x|
22
+ x.warmup = 10 if RUBY_ENGINE == 'jruby'
23
+ x.report('a: 2x around, 1x before, 1x after') { Speed.a(1) }
24
+ x.report('b: 1x around, 1x before, 1x after') { Speed.b(1) }
25
+ x.report('c: 1x before, 1x after') { Speed.c(1) }
26
+ x.report('d: 1x after') { Speed.d(1) }
27
+ x.report('e: 1x before') { Speed.e(1) }
28
+ x.report('f: 1x around') { Speed.f(1) }
29
+ x.report('g: no wrappers') { Speed.g(1) }
30
+ end
31
+
32
+ Speed.wrappers []
33
+ print 'method dispatching w/ wrappers'
34
+ Benchmark.ips do |x|
35
+ x.warmup = 10 if RUBY_ENGINE == 'jruby'
36
+ x.report('via receptacle') { Speed.a(:foo) }
37
+ x.report('direct via public_send') { Speed::Strategy::One.new.public_send(:a, :foo) }
38
+ x.report('direct via method-method') do
39
+ m = Speed::Strategy::One.new.method(:a)
40
+ m.call(:foo)
41
+ end
42
+ x.report('direct method-call') { Speed::Strategy::One.new.a(:foo) }
43
+ x.compare!
44
+ end
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # run with --profile.api in JRUBY_OPTS
4
+ require 'bundler/inline'
5
+ require 'jruby/profiler'
6
+ PROFILE_NAME = 'receptacle'
7
+
8
+ gemfile false do
9
+ source 'https://rubygems.org'
10
+ gem 'receptacle', path: './..'
11
+ end
12
+ require_relative 'speed_receptacle'
13
+
14
+ Speed.strategy(Speed::Strategy::One)
15
+ Speed.wrappers [Speed::Wrappers::W1,
16
+ Speed::Wrappers::W2,
17
+ Speed::Wrappers::W3,
18
+ Speed::Wrappers::W4,
19
+ Speed::Wrappers::W5,
20
+ Speed::Wrappers::W6]
21
+ Speed.a(1)
22
+ Speed.b(1)
23
+ Speed.c(1)
24
+ Speed.d(1)
25
+ Speed.e(1)
26
+ Speed.f(1)
27
+ Speed.g(1)
28
+
29
+ GC.disable
30
+ profile_data = JRuby::Profiler.profile do
31
+ 100_000.times { Speed.a(1) }
32
+ end
33
+
34
+ profile_printer = JRuby::Profiler::GraphProfilePrinter.new(profile_data)
35
+ profile_printer.printProfile(File.open("#{PROFILE_NAME}.graph.profile", 'w+'))
36
+ profile_printer.printProfile(STDOUT)
37
+
38
+ profile_printer = JRuby::Profiler::FlatProfilePrinter.new(profile_data)
39
+ profile_printer.printProfile(File.open("#{PROFILE_NAME}.flat.profile", 'w+'))
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+ require 'receptacle'
3
+ module Speed
4
+ include Receptacle::Repo
5
+ mediate :a
6
+ mediate :b
7
+ mediate :c
8
+ mediate :d
9
+ mediate :e
10
+ mediate :f
11
+ mediate :g
12
+ module Strategy
13
+ class One
14
+ def a(arg)
15
+ arg
16
+ end
17
+ alias b a
18
+ alias c a
19
+ alias d a
20
+ alias e a
21
+ alias f a
22
+ alias g a
23
+ end
24
+ end
25
+
26
+ module Wrappers
27
+ class W1
28
+ def before_a(args)
29
+ args
30
+ end
31
+
32
+ def after_a(return_values, _)
33
+ return_values
34
+ end
35
+
36
+ def before_f(args)
37
+ args
38
+ end
39
+
40
+ def after_f(return_values, _)
41
+ return_values
42
+ end
43
+ end
44
+
45
+ class W2
46
+ # :a
47
+ def before_a(args)
48
+ args
49
+ end
50
+
51
+ def after_a(return_values, _)
52
+ return_values
53
+ end
54
+
55
+ # :b
56
+ def before_b(args)
57
+ args
58
+ end
59
+
60
+ def after_b(return_values, _)
61
+ return_values
62
+ end
63
+ end
64
+ class W3
65
+ def before_a(args)
66
+ args
67
+ end
68
+
69
+ def before_c(args)
70
+ args
71
+ end
72
+ end
73
+
74
+ class W4
75
+ def after_a(return_values, _)
76
+ return_values
77
+ end
78
+
79
+ def after_d(return_value, _)
80
+ return_value
81
+ end
82
+ end
83
+
84
+ class W5
85
+ def before_b(args)
86
+ args
87
+ end
88
+
89
+ def after_c(return_value, _)
90
+ return_value
91
+ end
92
+ end
93
+
94
+ class W6
95
+ def after_b(return_value, _)
96
+ return_value
97
+ end
98
+
99
+ def before_e(args)
100
+ args
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'receptacle/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'receptacle'
9
+ spec.version = Receptacle::VERSION
10
+ spec.authors = ['Andreas Eger']
11
+ spec.email = ['dev@eger-andreas.de']
12
+
13
+ spec.summary = 'repository pattern'
14
+ spec.description = 'provides functionality for the repository or strategy pattern'
15
+ spec.homepage = 'https://github.com/andreaseger/receptacle'
16
+ spec.license = 'MIT'
17
+
18
+ spec.required_ruby_version = '~> 2.2'
19
+
20
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
21
+ f.match(%r{^(test|spec|features)/})
22
+ end
23
+ spec.bindir = 'exe'
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.13'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ spec.add_development_dependency 'minitest', '~> 5.0'
30
+ spec.add_development_dependency 'pry'
31
+ spec.add_development_dependency 'rubocop_runner', '~> 2.0'
32
+ spec.add_development_dependency 'rubocop', '~> 0.46.0'
33
+ spec.add_development_dependency 'simplecov', '~> 0.13'
34
+ spec.add_development_dependency 'codecov'
35
+ spec.add_development_dependency 'guard'
36
+ spec.add_development_dependency 'guard-minitest'
37
+ spec.add_development_dependency 'guard-rubocop'
38
+ end