├── .gitignore ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemfiles ├── rails3.gemfile ├── rails3.gemfile.lock ├── rails4.0.gemfile ├── rails4.0.gemfile.lock ├── rails4.1.gemfile └── rails4.1.gemfile.lock ├── houser.gemspec ├── lib ├── houser.rb └── houser │ ├── middleware.rb │ └── version.rb └── spec └── houser_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: 'bundle exec rspec spec' 2 | gemfile: 3 | - gemfiles/rails3.gemfile 4 | - gemfiles/rails4.0.gemfile 5 | - gemfiles/rails4.1.gemfile 6 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails3" do 2 | gem "rails", "3.2.18" 3 | end 4 | 5 | appraise "rails4.0" do 6 | gem "rails", "4.0.10" 7 | end 8 | 9 | appraise "rails4.1" do 10 | gem "rails", "4.1.2" 11 | end 12 | 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 2 | 3 | * X-Headers are deprecated (see [#6](https://github.com/radar/houser/issues/6)). Headers are now to be referenced as `Houser-Object` and `Houser-Subdomain`. 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Houser is an open source project and we encourage contributions. 2 | 3 | ## Filing an issue 4 | 5 | When filing an issue on the Houser project, please provide these details: 6 | 7 | * A comprehensive list of steps to reproduce the issue. 8 | * What you're *expecting* to happen compared with what's *actually* happening. 9 | * Your application's complete Gemfile, as text (*not as an image*) 10 | * Any relevant stack traces ("Full trace" preferred) 11 | 12 | In 99% of cases, this information is enough to determine the cause and solution 13 | to the problem that is being described. 14 | 15 | Please remember to format code using triple backticks (\`\`\`) so that it is neatly 16 | formatted when the issue is posted. 17 | 18 | Any issue that is open for 14 days without actionable information or activity 19 | will be marked as "stalled" and then closed. Stalled issues can be re-opened if 20 | the information requested is provided. 21 | 22 | ## Pull requests 23 | 24 | We gladly accept pull requests to fix bugs and, in some circumstances, add new 25 | features to Houser. 26 | 27 | Here's a quick guide: 28 | 29 | 1. Fork the repo. 30 | 31 | 2. Run the tests. We only take pull requests with passing tests, and it's great 32 | to know that you have a clean slate: 33 | 34 | $ bundle exec rspec spec 35 | 36 | 3. Create new branch then make changes and add tests for your changes. Only 37 | refactoring and documentation changes require no new tests. If you are adding 38 | functionality or fixing a bug, we need tests! 39 | 40 | Some things that will increase the chance that your pull request is accepted, 41 | taken straight from the Ruby on Rails guide: 42 | 43 | * Use Rails idioms and helpers 44 | * Include tests that fail without your code, and pass with it 45 | * Update the documentation, the surrounding one, examples elsewhere, guides, 46 | whatever is affected by your contribution 47 | 48 | Syntax: 49 | 50 | * Two spaces, no tabs. 51 | * No trailing whitespace. Blank lines should not have any space. 52 | * Prefer &&/|| over and/or. 53 | * `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. 54 | * `a = b` and not `a=b`. 55 | * `a_method { |block| ... }` and not `a_method { | block | ... }` 56 | * Follow the conventions you see used in the source already. 57 | * -> symbol over lambda 58 | * Ruby 1.9 hash syntax over Ruby 1.8 hash syntax 59 | 60 | And in case we didn't emphasize it enough: we love tests! 61 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in houser.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ryan Bigg 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. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Houser 2 | 3 | ![Build Status](https://api.travis-ci.org/radar/houser.png?branch=master) 4 | 5 | This is the multitenancy gem that is used in the Multitenancy with Rails book as an alternative method to PostgreSQL schemas which can have their own set of problems (such as backups taking an inordinately long time). 6 | 7 | Houser provides you with two Rack environment variables which can then be used in your application to scope resources correctly. That is all it does for the time being, and it will probably do more in the future. 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | gem 'houser' 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | In `config/application.rb`, put this line: 20 | 21 | ``` ruby 22 | config.middleware.use Houser::Middleware, 23 | :class_name => 'Model' 24 | ``` 25 | 26 | Where 'Model' is the class that you're scoping by. 27 | 28 | If you're using a TLD like `.co.uk` instead of `.com`, you will need to specify `tld_length` too: 29 | 30 | ``` ruby 31 | config.middleware.use Houser::Middleware, 32 | :class_name => 'Model', 33 | :tld_length => 2 34 | ``` 35 | 36 | ## Usage 37 | 38 | There are two rack environment variables set by the Houser middleware that you can use throughout your application to scope resources. 39 | 40 | If no object is found for the subdomain the request is received on, both of these variables will be nil. 41 | 42 | ### `env['Houser-Subdomain']` 43 | 44 | The complete subdomain of the request. The following domains have the following subdomains: 45 | 46 | * `sub1.example.com => sub1` 47 | * `sub2.sub1.example.com => sub2.sub1` 48 | * `sub1.example.co.uk => sub1` 49 | * `sub2.sub1.example.co.uk => sub2.sub1` 50 | 51 | ### `env['Houser-Object']` 52 | 53 | The instance of the Class that is found based on the subdomain. 54 | 55 | ## Contributing 56 | 57 | Please see [CONTRIBUTING.md](https://github.com/radar/houser/CONTRIBUTING.md) 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'appraisal' 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec 8 | -------------------------------------------------------------------------------- /gemfiles/rails3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "3.2.18" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails3.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | houser (2.0.0) 5 | rack 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionmailer (3.2.18) 11 | actionpack (= 3.2.18) 12 | mail (~> 2.5.4) 13 | actionpack (3.2.18) 14 | activemodel (= 3.2.18) 15 | activesupport (= 3.2.18) 16 | builder (~> 3.0.0) 17 | erubis (~> 2.7.0) 18 | journey (~> 1.0.4) 19 | rack (~> 1.4.5) 20 | rack-cache (~> 1.2) 21 | rack-test (~> 0.6.1) 22 | sprockets (~> 2.2.1) 23 | activemodel (3.2.18) 24 | activesupport (= 3.2.18) 25 | builder (~> 3.0.0) 26 | activerecord (3.2.18) 27 | activemodel (= 3.2.18) 28 | activesupport (= 3.2.18) 29 | arel (~> 3.0.2) 30 | tzinfo (~> 0.3.29) 31 | activeresource (3.2.18) 32 | activemodel (= 3.2.18) 33 | activesupport (= 3.2.18) 34 | activesupport (3.2.18) 35 | i18n (~> 0.6, >= 0.6.4) 36 | multi_json (~> 1.0) 37 | appraisal (1.0.2) 38 | bundler 39 | rake 40 | thor (>= 0.14.0) 41 | arel (3.0.3) 42 | builder (3.0.4) 43 | coderay (1.1.0) 44 | diff-lcs (1.2.5) 45 | erubis (2.7.0) 46 | hike (1.2.3) 47 | i18n (0.6.11) 48 | journey (1.0.4) 49 | json (1.8.1) 50 | mail (2.5.4) 51 | mime-types (~> 1.16) 52 | treetop (~> 1.4.8) 53 | method_source (0.8.2) 54 | mime-types (1.25.1) 55 | multi_json (1.10.1) 56 | polyglot (0.3.5) 57 | pry (0.10.1) 58 | coderay (~> 1.1.0) 59 | method_source (~> 0.8.1) 60 | slop (~> 3.4) 61 | rack (1.4.5) 62 | rack-cache (1.2) 63 | rack (>= 0.4) 64 | rack-ssl (1.3.4) 65 | rack 66 | rack-test (0.6.2) 67 | rack (>= 1.0) 68 | rails (3.2.18) 69 | actionmailer (= 3.2.18) 70 | actionpack (= 3.2.18) 71 | activerecord (= 3.2.18) 72 | activeresource (= 3.2.18) 73 | activesupport (= 3.2.18) 74 | bundler (~> 1.0) 75 | railties (= 3.2.18) 76 | railties (3.2.18) 77 | actionpack (= 3.2.18) 78 | activesupport (= 3.2.18) 79 | rack-ssl (~> 1.3.2) 80 | rake (>= 0.8.7) 81 | rdoc (~> 3.4) 82 | thor (>= 0.14.6, < 2.0) 83 | rake (10.3.2) 84 | rdoc (3.12.2) 85 | json (~> 1.4) 86 | rspec-core (3.0.4) 87 | rspec-support (~> 3.0.0) 88 | rspec-expectations (3.0.4) 89 | diff-lcs (>= 1.2.0, < 2.0) 90 | rspec-support (~> 3.0.0) 91 | rspec-mocks (3.0.4) 92 | rspec-support (~> 3.0.0) 93 | rspec-rails (3.0.1) 94 | actionpack (>= 3.0) 95 | activesupport (>= 3.0) 96 | railties (>= 3.0) 97 | rspec-core (~> 3.0.0) 98 | rspec-expectations (~> 3.0.0) 99 | rspec-mocks (~> 3.0.0) 100 | rspec-support (~> 3.0.0) 101 | rspec-support (3.0.4) 102 | slop (3.6.0) 103 | sprockets (2.2.2) 104 | hike (~> 1.2) 105 | multi_json (~> 1.0) 106 | rack (~> 1.0) 107 | tilt (~> 1.1, != 1.3.0) 108 | thor (0.19.1) 109 | tilt (1.4.1) 110 | treetop (1.4.15) 111 | polyglot 112 | polyglot (>= 0.3.1) 113 | tzinfo (0.3.41) 114 | 115 | PLATFORMS 116 | ruby 117 | 118 | DEPENDENCIES 119 | appraisal (= 1.0.2) 120 | bundler (~> 1.5) 121 | houser! 122 | pry 123 | rails (= 3.2.18) 124 | rake 125 | rspec-rails (= 3.0.1) 126 | -------------------------------------------------------------------------------- /gemfiles/rails4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "4.0.10" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails4.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | houser (2.0.0) 5 | rack 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionmailer (4.0.10) 11 | actionpack (= 4.0.10) 12 | mail (~> 2.5, >= 2.5.4) 13 | actionpack (4.0.10) 14 | activesupport (= 4.0.10) 15 | builder (~> 3.1.0) 16 | erubis (~> 2.7.0) 17 | rack (~> 1.5.2) 18 | rack-test (~> 0.6.2) 19 | activemodel (4.0.10) 20 | activesupport (= 4.0.10) 21 | builder (~> 3.1.0) 22 | activerecord (4.0.10) 23 | activemodel (= 4.0.10) 24 | activerecord-deprecated_finders (~> 1.0.2) 25 | activesupport (= 4.0.10) 26 | arel (~> 4.0.0) 27 | activerecord-deprecated_finders (1.0.3) 28 | activesupport (4.0.10) 29 | i18n (~> 0.6, >= 0.6.9) 30 | minitest (~> 4.2) 31 | multi_json (~> 1.3) 32 | thread_safe (~> 0.1) 33 | tzinfo (~> 0.3.37) 34 | appraisal (1.0.2) 35 | bundler 36 | rake 37 | thor (>= 0.14.0) 38 | arel (4.0.2) 39 | builder (3.1.4) 40 | coderay (1.1.0) 41 | diff-lcs (1.2.5) 42 | erubis (2.7.0) 43 | hike (1.2.3) 44 | i18n (0.6.11) 45 | mail (2.6.1) 46 | mime-types (>= 1.16, < 3) 47 | method_source (0.8.2) 48 | mime-types (2.3) 49 | minitest (4.7.5) 50 | multi_json (1.10.1) 51 | pry (0.10.1) 52 | coderay (~> 1.1.0) 53 | method_source (~> 0.8.1) 54 | slop (~> 3.4) 55 | rack (1.5.2) 56 | rack-test (0.6.2) 57 | rack (>= 1.0) 58 | rails (4.0.10) 59 | actionmailer (= 4.0.10) 60 | actionpack (= 4.0.10) 61 | activerecord (= 4.0.10) 62 | activesupport (= 4.0.10) 63 | bundler (>= 1.3.0, < 2.0) 64 | railties (= 4.0.10) 65 | sprockets-rails (~> 2.0) 66 | railties (4.0.10) 67 | actionpack (= 4.0.10) 68 | activesupport (= 4.0.10) 69 | rake (>= 0.8.7) 70 | thor (>= 0.18.1, < 2.0) 71 | rake (10.3.2) 72 | rspec-core (3.0.4) 73 | rspec-support (~> 3.0.0) 74 | rspec-expectations (3.0.4) 75 | diff-lcs (>= 1.2.0, < 2.0) 76 | rspec-support (~> 3.0.0) 77 | rspec-mocks (3.0.4) 78 | rspec-support (~> 3.0.0) 79 | rspec-rails (3.0.1) 80 | actionpack (>= 3.0) 81 | activesupport (>= 3.0) 82 | railties (>= 3.0) 83 | rspec-core (~> 3.0.0) 84 | rspec-expectations (~> 3.0.0) 85 | rspec-mocks (~> 3.0.0) 86 | rspec-support (~> 3.0.0) 87 | rspec-support (3.0.4) 88 | slop (3.6.0) 89 | sprockets (2.12.2) 90 | hike (~> 1.2) 91 | multi_json (~> 1.0) 92 | rack (~> 1.0) 93 | tilt (~> 1.1, != 1.3.0) 94 | sprockets-rails (2.1.4) 95 | actionpack (>= 3.0) 96 | activesupport (>= 3.0) 97 | sprockets (~> 2.8) 98 | thor (0.19.1) 99 | thread_safe (0.3.4) 100 | tilt (1.4.1) 101 | tzinfo (0.3.41) 102 | 103 | PLATFORMS 104 | ruby 105 | 106 | DEPENDENCIES 107 | appraisal (= 1.0.2) 108 | bundler (~> 1.5) 109 | houser! 110 | pry 111 | rails (= 4.0.10) 112 | rake 113 | rspec-rails (= 3.0.1) 114 | -------------------------------------------------------------------------------- /gemfiles/rails4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "4.1.2" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails4.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | houser (2.0.0) 5 | rack 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionmailer (4.1.2) 11 | actionpack (= 4.1.2) 12 | actionview (= 4.1.2) 13 | mail (~> 2.5.4) 14 | actionpack (4.1.2) 15 | actionview (= 4.1.2) 16 | activesupport (= 4.1.2) 17 | rack (~> 1.5.2) 18 | rack-test (~> 0.6.2) 19 | actionview (4.1.2) 20 | activesupport (= 4.1.2) 21 | builder (~> 3.1) 22 | erubis (~> 2.7.0) 23 | activemodel (4.1.2) 24 | activesupport (= 4.1.2) 25 | builder (~> 3.1) 26 | activerecord (4.1.2) 27 | activemodel (= 4.1.2) 28 | activesupport (= 4.1.2) 29 | arel (~> 5.0.0) 30 | activesupport (4.1.2) 31 | i18n (~> 0.6, >= 0.6.9) 32 | json (~> 1.7, >= 1.7.7) 33 | minitest (~> 5.1) 34 | thread_safe (~> 0.1) 35 | tzinfo (~> 1.1) 36 | appraisal (1.0.2) 37 | bundler 38 | rake 39 | thor (>= 0.14.0) 40 | arel (5.0.1.20140414130214) 41 | builder (3.2.2) 42 | coderay (1.1.0) 43 | diff-lcs (1.2.5) 44 | erubis (2.7.0) 45 | hike (1.2.3) 46 | i18n (0.6.11) 47 | json (1.8.1) 48 | mail (2.5.4) 49 | mime-types (~> 1.16) 50 | treetop (~> 1.4.8) 51 | method_source (0.8.2) 52 | mime-types (1.25.1) 53 | minitest (5.4.1) 54 | multi_json (1.10.1) 55 | polyglot (0.3.5) 56 | pry (0.10.1) 57 | coderay (~> 1.1.0) 58 | method_source (~> 0.8.1) 59 | slop (~> 3.4) 60 | rack (1.5.2) 61 | rack-test (0.6.2) 62 | rack (>= 1.0) 63 | rails (4.1.2) 64 | actionmailer (= 4.1.2) 65 | actionpack (= 4.1.2) 66 | actionview (= 4.1.2) 67 | activemodel (= 4.1.2) 68 | activerecord (= 4.1.2) 69 | activesupport (= 4.1.2) 70 | bundler (>= 1.3.0, < 2.0) 71 | railties (= 4.1.2) 72 | sprockets-rails (~> 2.0) 73 | railties (4.1.2) 74 | actionpack (= 4.1.2) 75 | activesupport (= 4.1.2) 76 | rake (>= 0.8.7) 77 | thor (>= 0.18.1, < 2.0) 78 | rake (10.3.2) 79 | rspec-core (3.0.4) 80 | rspec-support (~> 3.0.0) 81 | rspec-expectations (3.0.4) 82 | diff-lcs (>= 1.2.0, < 2.0) 83 | rspec-support (~> 3.0.0) 84 | rspec-mocks (3.0.4) 85 | rspec-support (~> 3.0.0) 86 | rspec-rails (3.0.1) 87 | actionpack (>= 3.0) 88 | activesupport (>= 3.0) 89 | railties (>= 3.0) 90 | rspec-core (~> 3.0.0) 91 | rspec-expectations (~> 3.0.0) 92 | rspec-mocks (~> 3.0.0) 93 | rspec-support (~> 3.0.0) 94 | rspec-support (3.0.4) 95 | slop (3.6.0) 96 | sprockets (2.12.2) 97 | hike (~> 1.2) 98 | multi_json (~> 1.0) 99 | rack (~> 1.0) 100 | tilt (~> 1.1, != 1.3.0) 101 | sprockets-rails (2.1.4) 102 | actionpack (>= 3.0) 103 | activesupport (>= 3.0) 104 | sprockets (~> 2.8) 105 | thor (0.19.1) 106 | thread_safe (0.3.4) 107 | tilt (1.4.1) 108 | treetop (1.4.15) 109 | polyglot 110 | polyglot (>= 0.3.1) 111 | tzinfo (1.2.2) 112 | thread_safe (~> 0.1) 113 | 114 | PLATFORMS 115 | ruby 116 | 117 | DEPENDENCIES 118 | appraisal (= 1.0.2) 119 | bundler (~> 1.5) 120 | houser! 121 | pry 122 | rails (= 4.1.2) 123 | rake 124 | rspec-rails (= 3.0.1) 125 | -------------------------------------------------------------------------------- /houser.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'houser/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "houser" 8 | spec.version = Houser::VERSION 9 | spec.authors = ["Ryan Bigg"] 10 | spec.email = ["radarlistener@gmail.com"] 11 | spec.summary = %q{Lightweight multitenancy gem.} 12 | spec.description = %q{Lightweight multitenancy gem.} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "rack" 22 | 23 | spec.add_development_dependency "bundler", "~> 1.5" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec-rails", "3.0.1" 26 | spec.add_development_dependency "pry" 27 | spec.add_development_dependency 'appraisal', '1.0.2' 28 | end 29 | -------------------------------------------------------------------------------- /lib/houser.rb: -------------------------------------------------------------------------------- 1 | require "houser/version" 2 | require "houser/middleware" 3 | 4 | module Houser 5 | end 6 | -------------------------------------------------------------------------------- /lib/houser/middleware.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | 3 | module Houser 4 | class Middleware 5 | attr_accessor :options 6 | 7 | def initialize(app, options={}) 8 | @options = options 9 | @options[:subdomain_column] ||= "subdomain" 10 | @options[:class] = Object.const_get(options[:class_name]) 11 | @options[:tld_length] ||= 1 12 | @app = app 13 | end 14 | 15 | def call(env) 16 | domain_parts = env['HTTP_HOST'].split('.') 17 | 18 | if domain_parts.length > 1 + options[:tld_length] 19 | domain_name = domain_parts[(-options[:tld_length] - 1)..-1] 20 | subdomain = (domain_parts - domain_name).join('.') 21 | find_tenant(env, subdomain) 22 | end 23 | 24 | @app.call(env) 25 | end 26 | 27 | private 28 | 29 | def find_tenant(env, subdomain) 30 | object = options[:class].where(options[:subdomain_column] => subdomain).first 31 | if object 32 | env['Houser-Subdomain'] = subdomain 33 | env['Houser-Object'] = object 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/houser/version.rb: -------------------------------------------------------------------------------- 1 | module Houser 2 | VERSION = "2.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/houser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'houser/middleware' 2 | require 'pry' 3 | 4 | # Inspired by http://taylorluk.com/post/54982679495/how-to-test-rack-middleware-with-rspec 5 | 6 | describe Houser::Middleware do 7 | let(:app) { ->(env) { [200, env, "app"] } } 8 | let(:options) do 9 | { 10 | class_name: 'Account' 11 | } 12 | end 13 | 14 | let(:middleware) do 15 | Houser::Middleware.new(app, options) 16 | end 17 | 18 | let(:subdomain) { 'account1' } 19 | 20 | before do 21 | stub_const('Account', Class.new) 22 | end 23 | 24 | def env_for(url, opts={}) 25 | opts.merge!('HTTP_HOST' => URI.parse(url).host) 26 | Rack::MockRequest.env_for(url, opts) 27 | end 28 | 29 | it "does nothing for non-subdomained requests" do 30 | expect(Account).to_not receive(:where) 31 | code, env = middleware.call(env_for("http://example.com")) 32 | expect(env['Houser-Subdomain']).to be_nil 33 | expect(env['Houser-Object']).to be_nil 34 | end 35 | 36 | it "sets Houser-ID header for known subdomains" do 37 | account = double(id: 1) 38 | expect(Account).to receive(:where).with("subdomain" => subdomain).and_return([account]) 39 | code, env = middleware.call(env_for("http://#{subdomain}.example.com")) 40 | expect(env['Houser-Subdomain']).to eq(subdomain) 41 | expect(env['Houser-Object']).to eq(account) 42 | end 43 | 44 | it "returns no headers for unknown subdomains" do 45 | expect(Account).to receive(:where).with("subdomain" => subdomain).and_return([]) 46 | code, env = middleware.call(env_for("http://#{subdomain}.example.com")) 47 | expect(env['Houser-Subdomain']).to be_nil 48 | expect(env['Houser-Object']).to be_nil 49 | end 50 | 51 | 52 | context "double subdomain" do 53 | let(:subdomain) { 'ruby.melbourne' } 54 | 55 | it "sets Houser-ID header for known subdomains within subdomains" do 56 | account = double(id: 1) 57 | expect(Account).to receive(:where).with("subdomain" => subdomain).and_return([account]) 58 | code, env = middleware.call(env_for("http://#{subdomain}.example.com")) 59 | expect(env['Houser-Subdomain']).to eq(subdomain) 60 | expect(env['Houser-Object']).to eq(account) 61 | end 62 | end 63 | 64 | context "with a different class name" do 65 | let(:options) do 66 | { 67 | class_name: 'Store' 68 | } 69 | end 70 | 71 | let(:store) { double(id: 2) } 72 | 73 | before do 74 | stub_const('Store', Class.new) 75 | end 76 | 77 | it "calls the right class" do 78 | expect(Account).to_not receive(:where) 79 | expect(Store).to receive(:where).with("subdomain" => subdomain).and_return([store]) 80 | code, env = middleware.call(env_for("http://#{subdomain}.example.com")) 81 | expect(env['Houser-Subdomain']).to eq(subdomain) 82 | expect(env['Houser-Object']).to eq(store) 83 | end 84 | end 85 | 86 | context "with more than one-level of TLD" do 87 | let(:options) do 88 | { 89 | tld_length: 2, 90 | class_name: 'Account' 91 | } 92 | end 93 | 94 | it "supports more than one-level of TLD" do 95 | expect(Account).to_not receive(:where) 96 | code, env = middleware.call(env_for("http://example.co.uk")) 97 | expect(env['Houser-Subdomain']).to be_nil 98 | expect(env['Houser-ID']).to be_nil 99 | end 100 | 101 | context "double subdomain" do 102 | let(:subdomain) { 'ruby.melbourne' } 103 | 104 | it "sets Houser-ID header for known subdomains within subdomains" do 105 | account = double(id: 1) 106 | expect(Account).to receive(:where).with("subdomain" => subdomain).and_return([account]) 107 | code, env = middleware.call(env_for("http://#{subdomain}.example.co.uk")) 108 | expect(env['Houser-Subdomain']).to eq(subdomain) 109 | expect(env['Houser-Object']).to eq(account) 110 | end 111 | end 112 | end 113 | 114 | context "with a different column name" do 115 | let(:options) do 116 | { 117 | subdomain_column: 'foo', 118 | class_name: 'Account' 119 | } 120 | end 121 | 122 | it "finds by an alternative column" do 123 | account = double(id: 1) 124 | expect(Account).to receive(:where).with("foo" => subdomain).and_return([account]) 125 | code, env = middleware.call(env_for("http://#{subdomain}.example.com")) 126 | expect(env['Houser-Subdomain']).to eq(subdomain) 127 | expect(env['Houser-Object']).to eq(account) 128 | end 129 | end 130 | end --------------------------------------------------------------------------------