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
@@ -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,5 @@
1
+ module Tenantify
2
+
3
+ VERSION = "0.0.1"
4
+
5
+ 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
@@ -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