receptacle 0.1.0

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.
@@ -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