tenantify 0.0.1

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +166 -0
  7. data/Rakefile +2 -0
  8. data/lib/tenantify.rb +57 -0
  9. data/lib/tenantify/configuration.rb +25 -0
  10. data/lib/tenantify/middleware.rb +40 -0
  11. data/lib/tenantify/middleware/builder.rb +63 -0
  12. data/lib/tenantify/middleware/strategies.rb +57 -0
  13. data/lib/tenantify/middleware/strategies/default.rb +46 -0
  14. data/lib/tenantify/middleware/strategies/header.rb +52 -0
  15. data/lib/tenantify/middleware/strategies/host.rb +57 -0
  16. data/lib/tenantify/resource.rb +28 -0
  17. data/lib/tenantify/tenant.rb +44 -0
  18. data/lib/tenantify/version.rb +5 -0
  19. data/spec/integration/manual_tenant_selection_spec.rb +16 -0
  20. data/spec/integration/middleware/custom_strategy.rb +40 -0
  21. data/spec/integration/middleware/many_strategies.rb +35 -0
  22. data/spec/integration/middleware/one_strategy_spec.rb +36 -0
  23. data/spec/integration/middleware/using_resources_spec.rb +48 -0
  24. data/spec/integration/resources_spec.rb +16 -0
  25. data/spec/spec_helper.rb +29 -0
  26. data/spec/tenantify/configuration_spec.rb +19 -0
  27. data/spec/tenantify/middleware/builder_spec.rb +66 -0
  28. data/spec/tenantify/middleware/strategies/default_spec.rb +17 -0
  29. data/spec/tenantify/middleware/strategies/header_spec.rb +27 -0
  30. data/spec/tenantify/middleware/strategies/host_spec.rb +32 -0
  31. data/spec/tenantify/middleware/strategies_spec.rb +41 -0
  32. data/spec/tenantify/middleware_spec.rb +37 -0
  33. data/spec/tenantify/resource_spec.rb +41 -0
  34. data/spec/tenantify/tenant_spec.rb +43 -0
  35. data/spec/tenantify/version_spec.rb +9 -0
  36. data/spec/tenantify_spec.rb +7 -0
  37. data/tenantify.gemspec +24 -0
  38. metadata +154 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8c5ba9f063bf0b429174f9c8e1c0ec83c7502bf8
4
+ data.tar.gz: 90a9332da5ce0b6a1ff840181ab141c45a3bf45c
5
+ SHA512:
6
+ metadata.gz: fff72aa956ebf8afcd1d6129a2b6114743b0514b3d79d01036cc85c32464e239e02341d1536d1c2fa3f9b945bbd8e19e9d0655df8f02728b8b372adf1fcbec25
7
+ data.tar.gz: 9f18246005abc4e3d0580ec40b443cc4b591e1c306e4c13747a701ce3cdb16d49139dbb8cf5ff90e29fc20f4459dd805418ec25f3405f6ddd7eb24502416273f
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tenantify.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Jaime Cabot Campins
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,166 @@
1
+ # Tenantify
2
+
3
+ This gem provides some tools to manage multitenancy on Ruby applications.
4
+
5
+ ## Synopsis
6
+
7
+ Tenantify is a tool to simplify multitenancy on Ruby applications.
8
+ It stores the current tenant in a thread variable and provides:
9
+
10
+ - A Rack middleware supporting some built-in and custom strategies to find a tenant
11
+ and set it as the current one for a request.
12
+
13
+ - A Resource class to encapsulate your application resources per tenant (databases,
14
+ external services, your own ruby instances, etc)
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'tenantify'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ ## Usage
29
+
30
+ ### The current tenant
31
+
32
+ Tenantify provides some class methods to set the current tenant for a piece of code.
33
+
34
+ To execute some code for a particular tenant:
35
+ ```ruby
36
+ Tenantify.using(:the_tenant) { # some code }
37
+ ```
38
+
39
+ After that the tenant is set to its previous value.
40
+
41
+ The using method returns the value returned by the block. If you want to get some data from
42
+ a database for a particular tenant:
43
+ ```ruby
44
+ data = Tenantify.using :the_tenant do
45
+ get_data_from_database
46
+ end
47
+ ```
48
+
49
+ To get the current tenant `Tenantify.current` is provided.
50
+
51
+ The `#using` method is the recommended way to run code for a particular tenant, but in some cases
52
+ may be useful to set a tenant as the current one from now on instead of just running a block of code.
53
+ For instance, in a pry session:
54
+ ```ruby
55
+ [1] pry(main)> Tenantify.use! :my_tenant
56
+ [2] pry(main)> Tenantify.current
57
+ => :my_tenant
58
+ ```
59
+
60
+ Example to show `Tenantify.using` and `Tenantify.use!` behaviour:
61
+ ```ruby
62
+ # No tenant is set by default
63
+ Tenantify.current # => nil
64
+
65
+ Tenantify.using :tenant_1 do
66
+ Tenantify.current # => :tenant_1
67
+
68
+ # Nested `using` blocks allowed
69
+ Tenantify.using :tenant_2 do
70
+ Tenantify.current # => :tenant_2
71
+
72
+ Tenantify.use! :tenant_3
73
+ Tenantify.current # => :tenant_3
74
+ end
75
+
76
+ # When a block ends the tenant before the block is set again
77
+ # even if it has changed inside the block due a `use!` call.
78
+ Tenantify.current # => :tenant_1
79
+ end
80
+
81
+ Tenantify.use! :tenant_4
82
+
83
+ # The current tenant is stored as a thread variable. On new threads it has to be set manually.
84
+ Thread.new do
85
+ Tenantify.current # => nil
86
+ end
87
+ Tenantify.current # => :tenant_4
88
+ ```
89
+
90
+ ### Resources
91
+
92
+ On your multitenant application some resources may depend on the current tenant. For instance: A Sequel database,
93
+ a redis database, the host of an external service, etc.
94
+
95
+ You could handle this situation by calling `Tenantify.current` to determine the resource you need for a tenant.
96
+ To avoid having to deal with the current tenant within your app business logic a `Tenantify::Resource` class is
97
+ provided.
98
+
99
+ To build a tenantified resource you have to build a hash that maps the tenant name to the resource for that tenant.
100
+ The following example shows how to configure a redis database per tenant on the same host.
101
+ ```ruby
102
+ # when initializing your application:
103
+ correspondence = {
104
+ :tenant_1 => Redis.new(:host => "localhost", :port => 6379, :db => 1),
105
+ :tenant_2 => Redis.new(:host => "localhost", :port => 6379, :db => 2)
106
+ }
107
+
108
+ redis_resource = Tenantify.resource(correspondence)
109
+ Object.const_set("Redis", redis_resource)
110
+
111
+
112
+ # at the entry point of your application
113
+ tenant_name = find_out_current_tenant
114
+ Tenantify.using(tenant_name) { app.run }
115
+
116
+
117
+ # anywhere inside your app
118
+ Redis.current # => Returns the redis instance for the current tenant
119
+ ```
120
+
121
+ You can build a resource for any multitenant resource you have on your application.
122
+
123
+ ### The Rack middleware
124
+
125
+ You can use Tenantify with any Ruby application you like and set the current tenant as soon as you know it,
126
+ ideally outside of the boundaries of your application business logic.
127
+
128
+ On a Rack application, this place is somewhere in the middleware stack. To handle this situation Tenantify
129
+ provides a `Tenantify::Middleware`.
130
+
131
+ There are several strategies you could use to choose the tenant to work with from the Rack environment.
132
+ Tenantify has some basic built-in strategies, but you might want to implement your custom ones to handle
133
+ more specific situations.
134
+
135
+ The built-in strategies are:
136
+
137
+ * The `:header` strategy expects the tenant name to be sent in a request header.
138
+ * The `:host` strategy expects a hash with tenants as keys, and arrays of hosts as values.
139
+ * The `:default` strategy always returns the same tenant.
140
+
141
+ You can configure Tenantify to use one or more strategies.
142
+
143
+ The following example configures Tenantify to:
144
+
145
+ * Check the "X-Tenant" header to look for a tenant.
146
+ * If the header does not exist, select a tenant associated to the current host.
147
+ * If no tenant provided for the current host, use de tenant: `:main_tenant`
148
+
149
+ ```ruby
150
+ # when initializing your application
151
+ hosts_per_tenant = {
152
+ :tenant_1 => ["www.host_a.com", "www.host_b.com"],
153
+ :tenant_2 => ["www.host_c.com"]
154
+ }
155
+
156
+ Tenantify.configure do |config|
157
+ config.strategy :header, :name => "X-Tenant"
158
+ config.strategy :hosts, hosts_per_tenant
159
+ config.strategy :default, :tenant => :main_tenant
160
+ end
161
+
162
+
163
+ # on your config.ru
164
+ use Tenantify::Middleware
165
+ run MyRackApplication
166
+ ```
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/lib/tenantify.rb ADDED
@@ -0,0 +1,57 @@
1
+ require "tenantify/version"
2
+
3
+ require "tenantify/configuration"
4
+ require "tenantify/tenant"
5
+ require "tenantify/resource"
6
+ require "tenantify/middleware"
7
+
8
+ module Tenantify
9
+ # Tenantify configuration
10
+ #
11
+ # @return [Configuration] the current configuration
12
+ def self.configuration
13
+ @configuration ||= Configuration.new
14
+ end
15
+
16
+ # A helper to configure Tenantify
17
+ #
18
+ # @yield [configuration] Configures tenantify
19
+ def self.configure
20
+ yield configuration
21
+ end
22
+
23
+ # An alias to {Tenant::using}
24
+ #
25
+ # @example Run some code on a particular tenant
26
+ # Tenantify.using :a_tenant do
27
+ # # some code...
28
+ # end
29
+ # @see Tenant.using
30
+ def self.using tenant, &block
31
+ Tenant.using(tenant, &block)
32
+ end
33
+
34
+ # An alias to {Tenant::use!}
35
+ #
36
+ # @example Change the current tenant
37
+ # Tenanfify.use! :a_tenant
38
+ # # using :a_tenant from now on
39
+ # @see Tenant.use!
40
+ def self.use! tenant
41
+ Tenant.use!(tenant)
42
+ end
43
+
44
+ # An alias to {Tenant::current}
45
+ #
46
+ # @see Tenant.current
47
+ def self.current
48
+ Tenant.current
49
+ end
50
+
51
+ # An alias to {Resource::new}
52
+ #
53
+ # @see Resource
54
+ def self.resource correspondence
55
+ Resource.new(correspondence)
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ module Tenantify
2
+ # It stores a configuration for {Tenantify::Middleware}.
3
+ class Configuration
4
+ # All configured strategies in order of priority.
5
+ #
6
+ # @return [Array<strategy_config>] a collection of strategy configurations.
7
+ attr_reader :strategies
8
+
9
+ # Constructor.
10
+ def initialize
11
+ @strategies = []
12
+ end
13
+
14
+ # Adds a new strategy for the Tenantify middleware. The order the strategies
15
+ # are added is the priority order they have to match the tenant.
16
+ #
17
+ # @param [Symbol, Class] the name of a known strategy or a custom strategy
18
+ # class.
19
+ # @param [Hash] strategy configuration.
20
+ # @return [Array<strategy_config>] a collection of strategy configurations.
21
+ def strategy name_or_class, strategy_config = {}
22
+ strategies << [name_or_class, strategy_config]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ require 'tenantify/tenant'
2
+ require 'tenantify/middleware/builder'
3
+
4
+ module Tenantify
5
+ # Rack middleware responsible for setting the tenant during the http request.
6
+ #
7
+ # This middleware builds a set of strategies from the given configuration, and
8
+ # sets the tenant returned from those strategies.
9
+ class Middleware
10
+ # Constructor.
11
+ #
12
+ # @param [#call] the Rack application
13
+ # @param [Tenantify::Configuration] the Rack application
14
+ def initialize app, config = Tenantify.configuration
15
+ @app = app
16
+ @config = config
17
+ end
18
+
19
+ # Calls the rack middleware.
20
+ #
21
+ # @param [rack_environment] the Rack environment
22
+ # @return [rack_response] the Rack response
23
+ def call env
24
+ tenant = strategies.tenant_for(env)
25
+
26
+ Tenant.using(tenant) { app.call(env) }
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :app, :config
32
+
33
+ def strategies
34
+ @strategies ||= begin
35
+ builder = Builder.new(config)
36
+ builder.call
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,63 @@
1
+ require 'tenantify/middleware/strategies'
2
+
3
+ strategies_pattern = File.join(File.dirname(__FILE__), "strategies/*.rb")
4
+ Dir[strategies_pattern].each { |strategy_file| require strategy_file }
5
+
6
+ module Tenantify
7
+ class Middleware
8
+ # This class builds all the strategies and injects them into a
9
+ # Strategies object.
10
+ class Builder
11
+ # Invalid strategy specification
12
+ UnknownStrategyError = Class.new(StandardError)
13
+
14
+ # Known strategies. They can be specified with a symbol.
15
+ KNOWN_STRATEGIES = {
16
+ :header => Strategies::Header,
17
+ :host => Strategies::Host,
18
+ :default => Strategies::Default
19
+ }
20
+
21
+ # @return [Tenantify::Configuration] given configuration.
22
+ attr_reader :config
23
+
24
+ # Constructor.
25
+ #
26
+ # @param [Tenantify::Configuration] the tenantify configuration.
27
+ # @param [Hash] a correspondence between strategy names and classes.
28
+ def initialize config, known_strategies: KNOWN_STRATEGIES
29
+ @config = config
30
+ @known_strategies = known_strategies
31
+ end
32
+
33
+ # Builds the Strategies object.
34
+ #
35
+ # @return [Strategies] the strategies object.
36
+ def call
37
+ Strategies.new(strategies)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :known_strategies
43
+
44
+ def strategies
45
+ strategies_config.map do |(name_or_class, strategy_config)|
46
+ strategy_class_for(name_or_class).new(strategy_config)
47
+ end
48
+ end
49
+
50
+ def strategy_class_for name_or_class
51
+ case name_or_class
52
+ when Class then name_or_class
53
+ when Symbol, String then known_strategies.fetch(name_or_class.to_sym)
54
+ else raise UnknownStrategyError.new(name_or_class.inspect)
55
+ end
56
+ end
57
+
58
+ def strategies_config
59
+ config.strategies
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,57 @@
1
+ module Tenantify
2
+ class Middleware
3
+ # Responsible for finding the tenant for the given env.
4
+ #
5
+ # It iterates the strategies given to the constructor until it finds
6
+ # one that returns a tenant.
7
+ #
8
+ # == Default strategy
9
+ # When there is no matching strategy for the current environment
10
+ # a NotMatchingStrategyError is raised. To avoid this behaviour and
11
+ # use a particular tenant by default a Default strategy is provided.
12
+ #
13
+ # @example Configuring a tenant by default:
14
+ # Tenantify.configure do |config|
15
+ # # your strategies
16
+ #
17
+ # config.strategy :default, :tenant => :my_default_tenant
18
+ # end
19
+ class Strategies
20
+ # None strategy found a valid tenant for the given environment
21
+ NotMatchingStrategyError = Class.new(StandardError)
22
+
23
+ # Constructor. It receives all strategies in order of precedence.
24
+ #
25
+ # @param [<#tenant_for>] enumerable of strategies
26
+ def initialize strategies
27
+ @strategies = strategies
28
+ end
29
+
30
+ # Find a tenant for the current env.
31
+ #
32
+ # @param [rack_environment] current env.
33
+ # @return [Symbol] returns the matching tenant.
34
+ def tenant_for env
35
+ find_tenant_for(env) or raise_error(env)
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :strategies
41
+
42
+ def find_tenant_for env
43
+ lazy_strategies.
44
+ map { |strategy| strategy.tenant_for(env) }.
45
+ find { |tenant| tenant }
46
+ end
47
+
48
+ def lazy_strategies
49
+ @lazy_strategies ||= strategies.lazy
50
+ end
51
+
52
+ def raise_error env
53
+ raise NotMatchingStrategyError.new(env.inspect)
54
+ end
55
+ end
56
+ end
57
+ end