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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +166 -0
- data/Rakefile +2 -0
- data/lib/tenantify.rb +57 -0
- data/lib/tenantify/configuration.rb +25 -0
- data/lib/tenantify/middleware.rb +40 -0
- data/lib/tenantify/middleware/builder.rb +63 -0
- data/lib/tenantify/middleware/strategies.rb +57 -0
- data/lib/tenantify/middleware/strategies/default.rb +46 -0
- data/lib/tenantify/middleware/strategies/header.rb +52 -0
- data/lib/tenantify/middleware/strategies/host.rb +57 -0
- data/lib/tenantify/resource.rb +28 -0
- data/lib/tenantify/tenant.rb +44 -0
- data/lib/tenantify/version.rb +5 -0
- data/spec/integration/manual_tenant_selection_spec.rb +16 -0
- data/spec/integration/middleware/custom_strategy.rb +40 -0
- data/spec/integration/middleware/many_strategies.rb +35 -0
- data/spec/integration/middleware/one_strategy_spec.rb +36 -0
- data/spec/integration/middleware/using_resources_spec.rb +48 -0
- data/spec/integration/resources_spec.rb +16 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/tenantify/configuration_spec.rb +19 -0
- data/spec/tenantify/middleware/builder_spec.rb +66 -0
- data/spec/tenantify/middleware/strategies/default_spec.rb +17 -0
- data/spec/tenantify/middleware/strategies/header_spec.rb +27 -0
- data/spec/tenantify/middleware/strategies/host_spec.rb +32 -0
- data/spec/tenantify/middleware/strategies_spec.rb +41 -0
- data/spec/tenantify/middleware_spec.rb +37 -0
- data/spec/tenantify/resource_spec.rb +41 -0
- data/spec/tenantify/tenant_spec.rb +43 -0
- data/spec/tenantify/version_spec.rb +9 -0
- data/spec/tenantify_spec.rb +7 -0
- data/tenantify.gemspec +24 -0
- metadata +154 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Tenantify
|
2
|
+
class Middleware
|
3
|
+
class Strategies
|
4
|
+
# Strategy to return always the same tenant.
|
5
|
+
#
|
6
|
+
# @example Using Default strategy:
|
7
|
+
# config = {:tenant => :a_tenant}
|
8
|
+
# strategy = Tenantify::Middleware::Strategies::Default.new(config)
|
9
|
+
#
|
10
|
+
# env = {} # any environment
|
11
|
+
# strategy.tenant_for(env) # => :a_tenant
|
12
|
+
class Default
|
13
|
+
# No tenant given.
|
14
|
+
NoTenantGivenError = Class.new(StandardError)
|
15
|
+
|
16
|
+
# Constructor.
|
17
|
+
#
|
18
|
+
# @param [Hash] the strategy configuration.
|
19
|
+
# @option :tenant the tenant to return
|
20
|
+
def initialize config
|
21
|
+
@config = config
|
22
|
+
end
|
23
|
+
|
24
|
+
# Finds a tenant for the given env.
|
25
|
+
#
|
26
|
+
# @param [rack_environment] the rack environment.
|
27
|
+
# @return [Symbol, nil] the found tenant of nil.
|
28
|
+
def tenant_for _env
|
29
|
+
tenant
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :config
|
35
|
+
|
36
|
+
def tenant
|
37
|
+
@tenant ||= config.fetch(:tenant) { raise_error }.to_sym
|
38
|
+
end
|
39
|
+
|
40
|
+
def raise_error
|
41
|
+
raise NoTenantGivenError
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Tenantify
|
2
|
+
class Middleware
|
3
|
+
class Strategies
|
4
|
+
# Strategy to get the tenant from a request header.
|
5
|
+
# It expect the tenant name from configuration.
|
6
|
+
#
|
7
|
+
# @example Using Header strategy:
|
8
|
+
# config = {:name => "X-Tenant"}
|
9
|
+
# strategy = Tenantify::Middleware::Strategies::Header.new(config)
|
10
|
+
#
|
11
|
+
# matching_env = {"X-Tenant" => "a_tenant"}
|
12
|
+
# strategy.tenant_for(matching_env) # => :a_tenant
|
13
|
+
#
|
14
|
+
# not_matching_env = {"X-Another-Header" => "something"}
|
15
|
+
# strategy.tenant_for(not_matching_env) # => nil
|
16
|
+
class Header
|
17
|
+
# No header name provided.
|
18
|
+
NoHeaderNameError = Class.new(StandardError)
|
19
|
+
|
20
|
+
# Constructor.
|
21
|
+
#
|
22
|
+
# @param [Hash] the strategy configuration.
|
23
|
+
# @option :name the name of the header.
|
24
|
+
def initialize config
|
25
|
+
@config = config
|
26
|
+
end
|
27
|
+
|
28
|
+
# Finds a tenant for the given env.
|
29
|
+
#
|
30
|
+
# @param [rack_environment] the rack environment.
|
31
|
+
# @return [Symbol, nil] the found tenant of nil.
|
32
|
+
def tenant_for env
|
33
|
+
tenant = env[header_name]
|
34
|
+
|
35
|
+
tenant.to_sym if tenant
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :config
|
41
|
+
|
42
|
+
def header_name
|
43
|
+
@header_name ||= config.fetch(:name) { raise_error }
|
44
|
+
end
|
45
|
+
|
46
|
+
def raise_error
|
47
|
+
raise NoHeaderNameError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Tenantify
|
2
|
+
class Middleware
|
3
|
+
class Strategies
|
4
|
+
# Strategy to get the tenant from the request host.
|
5
|
+
# It expect the tenant name from configuration.
|
6
|
+
#
|
7
|
+
# @example Using Host strategy:
|
8
|
+
# config = {
|
9
|
+
# :tenant_1 => ["www.domain_a.com", "www.domain_b.com"],
|
10
|
+
# :tenant_2 => ["www.domain_c.com"]
|
11
|
+
# }
|
12
|
+
# strategy = Tenantify::Middleware::Strategies::Host.new(config)
|
13
|
+
#
|
14
|
+
# matching_env = {"SERVER_NAME" => "www.domain_b.com"}
|
15
|
+
# strategy.tenant_for(matching_env) # => :tenant_1
|
16
|
+
#
|
17
|
+
# not_matching_env = {"SERVER_NAME" => "www.another_domain.com"}
|
18
|
+
# strategy.tenant_for(not_matching_env) # => nil
|
19
|
+
class Host
|
20
|
+
# Constructor. It receives a hash with tenants as keys and arrays of
|
21
|
+
# hosts as values.
|
22
|
+
#
|
23
|
+
# @param [{Symbol => <String>}] the strategy configuration.
|
24
|
+
def initialize config
|
25
|
+
@config = config
|
26
|
+
end
|
27
|
+
|
28
|
+
# Finds a tenant for the given env.
|
29
|
+
#
|
30
|
+
# @param [rack_environment] the rack environment.
|
31
|
+
# @return [Symbol, nil] the found tenant of nil.
|
32
|
+
def tenant_for env
|
33
|
+
host = env["SERVER_NAME"]
|
34
|
+
tenant = correspondence[host]
|
35
|
+
|
36
|
+
tenant.to_sym if tenant
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :config
|
42
|
+
|
43
|
+
def correspondence
|
44
|
+
@correspondence ||= config.reduce Hash.new do |result, (tenant, domains)|
|
45
|
+
result.merge! correspondence_for(tenant, domains)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def correspondence_for tenant, domains
|
50
|
+
domains.reduce Hash.new do |result, domain|
|
51
|
+
result.merge! domain => tenant
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'tenantify/tenant'
|
2
|
+
|
3
|
+
module Tenantify
|
4
|
+
class Resource
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize correspondence
|
8
|
+
@correspondence = correspondence
|
9
|
+
end
|
10
|
+
|
11
|
+
def current
|
12
|
+
correspondence.fetch(Tenant.current)
|
13
|
+
end
|
14
|
+
|
15
|
+
def all
|
16
|
+
correspondence
|
17
|
+
end
|
18
|
+
|
19
|
+
def each &block
|
20
|
+
correspondence.each &block
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :correspondence
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Tenantify
|
2
|
+
# Responsible for managing the current tenant. All useful methods {#using},
|
3
|
+
# {#use!}, and {#current} have aliases at {Tenantify} module.
|
4
|
+
#
|
5
|
+
# == Threading Notes
|
6
|
+
# The {Tenantify::Tenant} module uses thread variables to store the current
|
7
|
+
# tenant. This means that when a new thread is spawned, the tenant has to
|
8
|
+
# be set manually.
|
9
|
+
module Tenant
|
10
|
+
# Runs the given block for a tenant.
|
11
|
+
#
|
12
|
+
# @param [Symbol] the tenant to run the code for.
|
13
|
+
# @yield the code to run.
|
14
|
+
# @return the returning value of the block.
|
15
|
+
#
|
16
|
+
# @example Getting data from a storage of a particular tenant:
|
17
|
+
# data = Tenant.using :the_tenant do
|
18
|
+
# Storage.current.get_data
|
19
|
+
# end
|
20
|
+
def self.using tenant
|
21
|
+
original_tenant = Thread.current.thread_variable_get(:tenant)
|
22
|
+
|
23
|
+
Thread.current.thread_variable_set(:tenant, tenant)
|
24
|
+
yield
|
25
|
+
ensure
|
26
|
+
Thread.current.thread_variable_set(:tenant, original_tenant)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Sets the given tenant from now on.
|
30
|
+
#
|
31
|
+
# @param [Symbol] the tenant to set.
|
32
|
+
# @return [Symbol] the set tenant.
|
33
|
+
def self.use! tenant
|
34
|
+
Thread.current.thread_variable_set(:tenant, tenant)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the current tenant.
|
38
|
+
#
|
39
|
+
# @return [Symbol] the current tenant.
|
40
|
+
def self.current
|
41
|
+
Thread.current.thread_variable_get(:tenant)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'tenantify'
|
2
|
+
|
3
|
+
RSpec.describe "Manual tenant selection" do
|
4
|
+
|
5
|
+
it 'changes the current tenant properly' do
|
6
|
+
Tenantify.use! :tenant_1
|
7
|
+
expect(Tenantify.current).to eq :tenant_1
|
8
|
+
|
9
|
+
Tenantify.using :tenant_2 do
|
10
|
+
expect(Tenantify.current).to eq :tenant_2
|
11
|
+
end
|
12
|
+
|
13
|
+
expect(Tenantify.current).to eq :tenant_1
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'tenantify'
|
2
|
+
|
3
|
+
RSpec.describe "Many middleware strategies" do
|
4
|
+
|
5
|
+
let(:app) { double 'app' }
|
6
|
+
|
7
|
+
let :test_strategy do
|
8
|
+
Class.new do
|
9
|
+
def initialize config
|
10
|
+
end
|
11
|
+
|
12
|
+
def tenant_for env
|
13
|
+
:the_tenant
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
let :config do
|
19
|
+
Tenantify::Configuration.new.tap do |config|
|
20
|
+
config.strategy test_strategy
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:env) { double 'env' }
|
25
|
+
let(:response) { double 'response' }
|
26
|
+
|
27
|
+
it 'returns a matching tenant or raises_error' do
|
28
|
+
middleware = Tenantify::Middleware.new(app, config)
|
29
|
+
|
30
|
+
expect(app).to receive :call do |env|
|
31
|
+
expect(env).to eq env
|
32
|
+
expect(Tenantify.current).to eq :the_tenant
|
33
|
+
|
34
|
+
response
|
35
|
+
end
|
36
|
+
|
37
|
+
expect(middleware.call(env)).to eq response
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'tenantify'
|
2
|
+
|
3
|
+
RSpec.describe "Many middleware strategies" do
|
4
|
+
|
5
|
+
let(:app) { double 'app' }
|
6
|
+
|
7
|
+
let :hosts_config do
|
8
|
+
{:first_tenant => ["www.host_a.com", "www.host_b.com"],
|
9
|
+
:second_tenant => ["www.host_c.com", "www.host_d.com"]}
|
10
|
+
end
|
11
|
+
|
12
|
+
let :config do
|
13
|
+
Tenantify::Configuration.new.tap do |config|
|
14
|
+
config.strategy :header, :name => "X-Tenant"
|
15
|
+
config.strategy :host, hosts_config
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:env) { {"SERVER_NAME" => "www.host_c.com"} }
|
20
|
+
let(:response) { double 'response' }
|
21
|
+
|
22
|
+
it 'returns a matching tenant' do
|
23
|
+
middleware = Tenantify::Middleware.new(app, config)
|
24
|
+
|
25
|
+
expect(app).to receive :call do |env|
|
26
|
+
expect(env).to eq env
|
27
|
+
expect(Tenantify.current).to eq :second_tenant
|
28
|
+
|
29
|
+
response
|
30
|
+
end
|
31
|
+
|
32
|
+
expect(middleware.call(env)).to eq response
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'tenantify'
|
2
|
+
|
3
|
+
RSpec.describe "One middleware strategy" do
|
4
|
+
|
5
|
+
let(:app) { double 'app' }
|
6
|
+
|
7
|
+
let :config do
|
8
|
+
Tenantify::Configuration.new.tap do |config|
|
9
|
+
config.strategy :header, :name => "X-Tenant"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:valid_env) { {"X-Tenant" => "a_tenant"} }
|
14
|
+
let(:invalid_env) { {} }
|
15
|
+
|
16
|
+
let(:response) { double 'response' }
|
17
|
+
|
18
|
+
it 'returns a matching tenant or raises_error' do
|
19
|
+
middleware = Tenantify::Middleware.new(app, config)
|
20
|
+
|
21
|
+
# Valid env
|
22
|
+
expect(app).to receive :call do |env|
|
23
|
+
expect(env).to eq env
|
24
|
+
expect(Tenantify.current).to eq :a_tenant
|
25
|
+
|
26
|
+
response
|
27
|
+
end
|
28
|
+
|
29
|
+
expect(middleware.call(valid_env)).to eq response
|
30
|
+
|
31
|
+
# Invalid env
|
32
|
+
no_match_error = Tenantify::Middleware::Strategies::NotMatchingStrategyError
|
33
|
+
expect {middleware.call(invalid_env)}.to raise_error no_match_error
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'tenantify'
|
2
|
+
|
3
|
+
RSpec.describe "Using a resource with the middleware" do
|
4
|
+
|
5
|
+
let(:app) { double 'app' }
|
6
|
+
|
7
|
+
let :test_strategy do
|
8
|
+
Class.new do
|
9
|
+
def initialize config
|
10
|
+
end
|
11
|
+
|
12
|
+
def tenant_for env
|
13
|
+
:a_tenant
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
let :config do
|
19
|
+
Tenantify::Configuration.new.tap do |config|
|
20
|
+
config.strategy test_strategy
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:resource_1) { double 'resource_1' }
|
25
|
+
let(:resource_2) { double 'resource_2' }
|
26
|
+
|
27
|
+
let :resource do
|
28
|
+
Tenantify.resource :a_tenant => resource_1,
|
29
|
+
:another_tenant => resource_2
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:env) { double 'env' }
|
33
|
+
let(:response) { double 'response' }
|
34
|
+
|
35
|
+
it 'returns a matching tenant or raises_error' do
|
36
|
+
middleware = Tenantify::Middleware.new(app, config)
|
37
|
+
|
38
|
+
expect(app).to receive :call do |env|
|
39
|
+
expect(env).to eq env
|
40
|
+
expect(resource.current).to eq resource_1
|
41
|
+
|
42
|
+
response
|
43
|
+
end
|
44
|
+
|
45
|
+
expect(middleware.call(env)).to eq response
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'tenantify'
|
2
|
+
|
3
|
+
RSpec.describe "Resource creation" do
|
4
|
+
|
5
|
+
it 'changes the current tenant properly' do
|
6
|
+
Tenantify.use! :tenant_1
|
7
|
+
expect(Tenantify.current).to eq :tenant_1
|
8
|
+
|
9
|
+
Tenantify.using :tenant_2 do
|
10
|
+
expect(Tenantify.current).to eq :tenant_2
|
11
|
+
end
|
12
|
+
|
13
|
+
expect(Tenantify.current).to eq :tenant_1
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require 'tenantify/tenant'
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
|
6
|
+
# Restore the original tenant after each test
|
7
|
+
config.around :each do |example|
|
8
|
+
begin
|
9
|
+
original_value = Tenantify::Tenant.current
|
10
|
+
example.run
|
11
|
+
ensure
|
12
|
+
Tenantify::Tenant.use! original_value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
config.expect_with :rspec do |expectations|
|
17
|
+
# Best error messages on chained expectations
|
18
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
19
|
+
end
|
20
|
+
|
21
|
+
config.mock_with :rspec do |mocks|
|
22
|
+
# Prevents you from stubbing a method that does not exist on a real object
|
23
|
+
mocks.verify_partial_doubles = true
|
24
|
+
end
|
25
|
+
|
26
|
+
config.disable_monkey_patching!
|
27
|
+
config.order = :random
|
28
|
+
|
29
|
+
end
|