├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── activerecord-tableless.gemspec ├── features ├── basic_integration.feature ├── step_definitions │ ├── rails_steps.rb │ ├── tableless.rb │ └── web_steps.rb └── support │ ├── env.rb │ ├── paths.rb │ ├── rails.rb │ └── selectors.rb ├── gemfiles ├── rails30.gemfile ├── rails32.gemfile ├── rails40.gemfile ├── rails41.gemfile └── rails42.gemfile ├── init.rb ├── lib ├── activerecord-tableless.rb └── activerecord-tableless │ └── version.rb └── spec ├── lib └── activerecord-tableless_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | nbproject 2 | tmp 3 | *.gem 4 | *emfile.lock 5 | 6 | # For emacs: 7 | *~ 8 | #\#* 9 | #.\#* 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color --backtrace --format doc 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | #bundler_args: --without development 3 | before_install: 4 | - rvm @global do gem uninstall bundler --all --executables 5 | - rvm @global do gem install bundler -v '~> 1.12.0' 6 | - bundle --version 7 | rvm: 8 | - 2.2.2 9 | - 2.2.7 10 | - 2.3.0 11 | - 2.3.4 12 | gemfile: 13 | - gemfiles/rails30.gemfile 14 | - gemfiles/rails32.gemfile 15 | - gemfiles/rails40.gemfile 16 | - gemfiles/rails41.gemfile 17 | - gemfiles/rails42.gemfile 18 | matrix: 19 | include: 20 | - rvm: 2.4.0 21 | gemfile: gemfiles/rails42.gemfile 22 | - rvm: 2.4.1 23 | gemfile: gemfiles/rails42.gemfile 24 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | #-*- ruby -*- 2 | 3 | appraise "rails30" do 4 | gem "rails", "~> 3.0.0" 5 | end 6 | 7 | # appraise "rails31" do 8 | # gem "rails", "~> 3.1.0" 9 | # end 10 | 11 | appraise "rails32" do 12 | gem "rails", "~> 3.2.0" 13 | gem "jquery-rails" 14 | end 15 | 16 | appraise "rails40" do 17 | gem "rails", "~> 4.0.0" 18 | gem "jquery-rails" 19 | end 20 | 21 | appraise "rails41" do 22 | gem "rails", "~> 4.1.0" 23 | gem "jquery-rails" 24 | end 25 | 26 | appraise "rails42" do 27 | gem "rails", "~> 4.2.0" 28 | gem "jquery-rails" 29 | end 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in utf8_enforcer_workaround.gemspec 4 | gemspec 5 | 6 | gem "mime-types", "< 2.0", :platform => :ruby_18 7 | gem "rubyzip", "< 1.0", :platform => :ruby_18 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | LICENSE 3 | 4 | The MIT License 5 | 6 | Copyright (c) 2008 Jarl Friis. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ActiveRecord Tableless 2 | ====================== 3 | 4 | [![Build Status](https://travis-ci.org/softace/activerecord-tableless.png)](http://travis-ci.org/softace/activerecord-tableless) 5 | [![Dependency Status](https://gemnasium.com/softace/activerecord-tableless.png)](https://gemnasium.com/softace/activerecord-tableless) 6 | [![Code Climate](https://codeclimate.com/github/softace/activerecord-tableless.png)](https://codeclimate.com/github/softace/activerecord-tableless) 7 | 8 | A single implementation of the ActiveRecord Tableless pattern for any 9 | Rails project or other Ruby project that uses ActiveRecord. 10 | 11 | Why, why, why 12 | ------------- 13 | 14 | Why would you ever consider this gem as opposed to ActiveModel. 15 | 16 | It all started in Rails 2 (see History section). In rails 2 ActiveModel did not exists. Nowadays rails 2.x series is no longer supported. 17 | 18 | In Rails 3 there is an Active Model API, where you can use some 19 | features of Active Record in other classes. Yehuda Katz has written 20 | [a nice introduction about this](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/). 21 | 22 | In Rails 4 basic features of the Active Model API can be activated 23 | by including 24 | [ActiveModel:Model](http://api.rubyonrails.org/classes/ActiveModel/Model.html) 25 | in the model. Carlos Antônio has written 26 | [a nice tutorial about this](http://blog.plataformatec.com.br/2012/03/barebone-models-to-use-with-actionpack-in-rails-4-0/). 27 | 28 | However neither the Active Model API (Rails 3) nor the 29 | ActiveModel::Model (Rails 4) supports relations and nested attributes. 30 | 31 | 32 | Installation 33 | ------------ 34 | 35 | ActiveRecord Tableless is distributed as a gem, which is how it should 36 | be used in your app. 37 | 38 | Include the gem in your Gemfile: 39 | 40 | gem "activerecord-tableless", "~> 1.0" 41 | 42 | 43 | Supported Versions 44 | ------------------ 45 | 46 | Supported ruby version are 47 | 48 | * **2.2.x** series higher than 2.2.2 49 | * **2.3.x** series 50 | 51 | If you are using Ruby version < 2.2.2 you can use the gem version < 52 | 2.0 like this 53 | 54 | gem "activerecord-tableless", "~> 1.0.0" 55 | 56 | Supported ActiveRecord versions are 57 | 58 | * **3.0.x** series 59 | * **3.2.x** series 60 | * **4.1.x** series 61 | * **4.2.x** series 62 | 63 | If you are using ActiveRecord 2.3.x series you can use the gem version < 64 | 2.0 like this 65 | 66 | gem "activerecord-tableless", "~> 1.0.0" 67 | 68 | You may be able to make it work with 3.1.x, but you should expect to 69 | put some time in it. 70 | 71 | TODO 72 | ---- 73 | 74 | * Support Rails 5.x series 75 | 76 | Usage 77 | ----- 78 | 79 | Define a model like this: 80 | 81 | class ContactMessage < ActiveRecord::Base 82 | has_no_table 83 | column :name, :string 84 | column :email, :string 85 | validates_presence_of :name, :email 86 | end 87 | 88 | You can now use the model in a view like this: 89 | 90 | <%= form_for :message, @message do |f| %> 91 | Your name: <%= f.text_field :name %> 92 | Your email: <%= f.text_field :email %> 93 | <% end %> 94 | 95 | And in the controller: 96 | 97 | def message 98 | @message = ContactMessage.new 99 | if request.post? 100 | @message.attributes = params[:message] 101 | if @message.valid? 102 | # Process the message... 103 | end 104 | end 105 | end 106 | 107 | If you wish (this is not recommended), you can pretend you have a succeeding database by using 108 | 109 | has_no_table :database => :pretend_success 110 | 111 | 112 | Development 113 | ----------- 114 | 115 | To start develop, please download the source code 116 | 117 | git clone git://github.com/softace/activerecord-tableless.git 118 | 119 | Install development libraries 120 | 121 | sudo apt-get install -y libsqlite3-dev libxml2-dev libxslt-dev 122 | 123 | When downloaded, you can start issuing the commands like 124 | 125 | gem install bundler -v '~> 1.12.0' 126 | bundle install 127 | bundle update 128 | bundle exec appraisal generate 129 | bundle exec appraisal install 130 | bundle exec appraisal rake all 131 | 132 | Or you can see what other options are there: 133 | 134 | bundle exec rake -T 135 | 136 | Publishing gem 137 | -------------- 138 | 139 | ``` 140 | gem bump -v pre 141 | ``` 142 | 143 | Verify everything is OK. 144 | 145 | ``` 146 | gem build activerecord-tableless.gemspec 147 | ``` 148 | 149 | Verify everything is OK. 150 | 151 | ``` 152 | gem release -t 153 | ``` 154 | 155 | 156 | History 157 | ------- 158 | 159 | Originally this code was implemented for Rails 2 by Kenneth 160 | Kalmer. For Rails 3 the need for this functionality was reduced 161 | dramatically due to the introduction of ActiveModel. But because the 162 | ActiveModel does not support relations and nested attributes the 163 | existence of this gem is still justified. 164 | 165 | For a history of technical implementation details feel free to take a 166 | look in the git log :-) 167 | 168 | 169 | Copyright 170 | --------- 171 | 172 | Copyright (c) Jarl Friis. See LICENSE.txt for 173 | further details. 174 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'appraisal' 3 | require 'rake/testtask' 4 | require 'rspec/core/rake_task' 5 | require 'cucumber/rake/task' 6 | 7 | desc 'Default: clean, all.' 8 | task :default => [:clean, :all] 9 | 10 | desc 'Test the activerecord-tableless on all supported Rails versions.' 11 | task :all do |t| 12 | if ENV['BUNDLE_GEMFILE'] 13 | exec('rake test spec cucumber') 14 | else 15 | Rake::Task["appraisal:install"].execute 16 | exec('rake appraisal test spec cucumber') 17 | end 18 | end 19 | 20 | desc 'Test the activerecord-tableless plugin.' 21 | Rake::TestTask.new(:test) do |t| 22 | t.libs << 'lib' << 'profile' 23 | t.pattern = 'test/**/*_test.rb' 24 | t.verbose = true 25 | end 26 | 27 | desc "Run specs" 28 | RSpec::Core::RakeTask.new do |t| 29 | t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default. 30 | # Put spec opts in a file named .rspec in root 31 | end 32 | 33 | desc 'Run integration test' 34 | Cucumber::Rake::Task.new do |t| 35 | t.cucumber_opts = %w{--format progress} 36 | end 37 | 38 | desc 'Clean up files.' 39 | task :clean do |t| 40 | FileUtils.rm_rf "doc" 41 | FileUtils.rm_rf "tmp" 42 | FileUtils.rm_rf "pkg" 43 | FileUtils.rm_rf "public" 44 | Dir.glob("activerecord-tableless-*.gem").each{|f| FileUtils.rm f } 45 | end 46 | -------------------------------------------------------------------------------- /activerecord-tableless.gemspec: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | $LOAD_PATH.push File.expand_path("../lib", __FILE__) 3 | require 'activerecord-tableless/version' 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'activerecord-tableless' 7 | gem.version = ActiveRecord::Tableless::VERSION 8 | gem.platform = Gem::Platform::RUBY 9 | gem.authors = ["Jarl Friis", "Kenneth Kalmer", "Michal Zima"] 10 | gem.email = ["jarl@softace.dk"] 11 | gem.homepage = "https://github.com/softace/activerecord-tableless" 12 | gem.summary = %q{A library for implementing tableless ActiveRecord models} 13 | gem.description = %q{ActiveRecord Tableless Models provides a simple mixin for creating models that are not bound to the database. This approach is useful for taking advantage of the features of ActiveRecord such as validation, relationships, etc.} 14 | gem.license = 'MIT' 15 | 16 | gem.files = `git ls-files`.split($\) 17 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 19 | gem.has_rdoc = true 20 | 21 | gem.require_paths = ["lib"] 22 | 23 | gem.add_runtime_dependency("activerecord", ">= 3.0.0", '< 5.0.0') 24 | 25 | gem.add_development_dependency('bundler', '> 0', '< 1.13.0') 26 | gem.add_development_dependency('rake', '~> 0', '> 0') 27 | 28 | # gem.add_development_dependency("rails") # This is in the appraisal gemfiles 29 | gem.add_development_dependency('sqlite3', '~> 1.3') 30 | 31 | gem.add_development_dependency('appraisal', '~> 1.0') 32 | gem.add_development_dependency('cucumber', '~> 1.1') 33 | gem.add_development_dependency("rspec", '~> 3.1') 34 | gem.add_development_dependency("rspec-collection_matchers", '~> 1.0') 35 | gem.add_development_dependency('aruba', '~> 0.5') 36 | 37 | gem.add_development_dependency('nokogiri', '~> 1.0') 38 | gem.add_development_dependency('capybara', '~> 0.0') 39 | gem.add_development_dependency('gem-release', '~> 0.7.4') 40 | 41 | # gem.add_development_dependency('launchy', '~> 2.1') 42 | # gem.add_development_dependency('debugger') 43 | end 44 | -------------------------------------------------------------------------------- /features/basic_integration.feature: -------------------------------------------------------------------------------- 1 | Feature: Rails integration 2 | 3 | Background: 4 | Given I generate a new rails application 5 | And I run a "scaffold" generator to generate a "User" scaffold with "name:string" 6 | And I delete all migrations 7 | And I update my new user model to be tableless 8 | And I update my users controller to render instead of redirect 9 | 10 | Scenario: Work as normal model 11 | And I start the rails application 12 | When I go to the new user page 13 | And I fill in "Name" with "something" 14 | And I press "Create" 15 | Then I should see "Name: something" 16 | 17 | -------------------------------------------------------------------------------- /features/step_definitions/rails_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^I generate a new rails application$/ do 2 | steps %{ 3 | When I successfully run `bundle exec #{new_application_command(APP_NAME)}` 4 | And I cd to "#{APP_NAME}" 5 | And I turn off class caching 6 | And I fix the application.rb for 3.0.12 7 | And I write to "Gemfile" with: 8 | """ 9 | source "http://rubygems.org" 10 | gem "rails", "#{framework_version}" 11 | gem "sqlite3" 12 | gem "capybara" 13 | gem "gherkin" 14 | """ 15 | And I configure the application to use "activerecord-tableless" from this project 16 | And I reset Bundler environment variable 17 | And I successfully run `bundle install --local` 18 | } 19 | end 20 | 21 | Given "I fix the application.rb for 3.0.12" do 22 | ##See https://github.com/rails/rails/issues/9619 23 | in_current_dir do 24 | File.open("config/application.rb", "a") do |f| 25 | f << "ActionController::Base.config.relative_url_root = ''" 26 | end 27 | end 28 | end 29 | 30 | Given /^I run a "(.*?)" generator to generate a "(.*?)" scaffold with "(.*?)"$/ do |generator_name, model_name, attributes| 31 | step %[I successfully run `bundle exec #{generator_command} #{generator_name} #{model_name} #{attributes}`] 32 | end 33 | 34 | Given /^I start the rails application$/ do 35 | in_current_dir do 36 | require "./config/environment" 37 | require "capybara/rails" 38 | end 39 | end 40 | 41 | Given /^I reload my application$/ do 42 | Rails::Application.reload! 43 | end 44 | 45 | When %r{I turn off class caching} do 46 | in_current_dir do 47 | file = "config/environments/test.rb" 48 | config = IO.read(file) 49 | config.gsub!(%r{^\s*config.cache_classes.*$}, 50 | "config.cache_classes = false") 51 | File.open(file, "w"){|f| f.write(config) } 52 | end 53 | end 54 | 55 | Given /^I update my application to use Bundler$/ do 56 | if framework_version?("2") 57 | boot_config_template = File.read('features/support/fixtures/boot_config.txt') 58 | preinitializer_template = File.read('features/support/fixtures/preinitializer.txt') 59 | gemfile_template = File.read('features/support/fixtures/gemfile.txt') 60 | in_current_dir do 61 | content = File.read("config/boot.rb").sub(/Rails\.boot!/, boot_config_template) 62 | File.open("config/boot.rb", "w") { |file| file.write(content) } 63 | File.open("config/preinitializer.rb", "w") { |file| file.write(preinitializer_template) } 64 | File.open("Gemfile", "w") { |file| file.write(gemfile_template.sub(/RAILS_VERSION/, framework_version)) } 65 | end 66 | end 67 | end 68 | 69 | Then /^the file at "([^"]*)" should be the same as "([^"]*)"$/ do |web_file, path| 70 | expected = IO.binread(path) 71 | actual = if web_file.match %r{^https?://} 72 | Net::HTTP.get(URI.parse(web_file)) 73 | else 74 | visit(web_file) 75 | page.source 76 | end 77 | actual.should == expected 78 | end 79 | 80 | When /^I configure the application to use "([^\"]+)" from this project$/ do |name| 81 | append_to_gemfile "gem '#{name}', :path => '#{PROJECT_ROOT}'" 82 | steps %{And I run `bundle install --local`} 83 | end 84 | 85 | When /^I configure the application to use "([^\"]+)"$/ do |gem_name| 86 | append_to_gemfile "gem '#{gem_name}'" 87 | end 88 | 89 | When /^I append gems from Appraisal Gemfile$/ do 90 | File.read(ENV['BUNDLE_GEMFILE']).split(/\n/).each do |line| 91 | if line =~ /^gem "(?!rails|appraisal)/ 92 | append_to_gemfile line.strip 93 | end 94 | end 95 | end 96 | 97 | When /^I comment out the gem "(.*?)" from the Gemfile$/ do |gemname| 98 | comment_out_gem_in_gemfile gemname 99 | end 100 | 101 | Then /^the result of "(.*?)" should be the same as "(.*?)"$/ do |rails_expr, path| 102 | expected = IO.binread(path) 103 | actual = eval "#{rails_expr}" 104 | actual.should == expected 105 | end 106 | 107 | 108 | module FileHelpers 109 | def append_to(path, contents) 110 | in_current_dir do 111 | File.open(path, "a") do |file| 112 | file.puts 113 | file.puts contents 114 | end 115 | end 116 | end 117 | 118 | def append_to_gemfile(contents) 119 | append_to('Gemfile', contents) 120 | end 121 | 122 | def comment_out_gem_in_gemfile(gemname) 123 | in_current_dir do 124 | gemfile = File.read("Gemfile") 125 | gemfile.sub!(/^(\s*)(gem\s*['"]#{gemname})/, "\\1# \\2") 126 | File.open("Gemfile", 'w'){ |file| file.write(gemfile) } 127 | end 128 | end 129 | 130 | def transform_file(filename) 131 | if File.exists?(filename) 132 | content = File.read(filename) 133 | File.open(filename, "w") do |f| 134 | content = yield(content) 135 | f.write(content) 136 | end 137 | end 138 | end 139 | end 140 | 141 | World(FileHelpers) 142 | -------------------------------------------------------------------------------- /features/step_definitions/tableless.rb: -------------------------------------------------------------------------------- 1 | Given /^I delete all migrations$/ do 2 | steps %{ 3 | When I successfully run `bash -c 'rm db/migrate/*.rb'` 4 | } 5 | end 6 | 7 | Given /^I update my new user model to be tableless$/ do 8 | in_current_dir do 9 | file_name = 'app/models/user.rb' 10 | content = File.read(file_name) 11 | if framework_version < "3.0" 12 | content = "require 'activerecord-tableless'\n" + content 13 | end 14 | 15 | content.gsub!(/^(class .* < ActiveRecord::Base)$/, "\\1\n" + <<-TABLELESS) 16 | has_no_table 17 | column :id, :integer 18 | column :name, :string 19 | 20 | TABLELESS 21 | File.open(file_name, 'w') { |f| f << content } 22 | end 23 | end 24 | 25 | Given /^I update my users controller to render instead of redirect$/ do 26 | in_current_dir do 27 | transform_file('app/controllers/users_controller.rb') do |content| 28 | ##Changes in #create method 29 | content.gsub!(/@user = User.new\((.*?)\)/, 30 | '@user = User.new(\1); @user.id = 1') 31 | content.gsub!("if @user.save", 32 | "if @user.valid?") 33 | content.gsub!(/redirect_to([\( ])@user, .*?([\)]| \}|$)/, 34 | "render\\1:action => 'show'\\2") 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /features/step_definitions/web_steps.rb: -------------------------------------------------------------------------------- 1 | # TL;DR: YOU SHOULD DELETE THIS FILE 2 | # 3 | # This file was generated by Cucumber-Rails and is only here to get you a head start 4 | # These step definitions are thin wrappers around the Capybara/Webrat API that lets you 5 | # visit pages, interact with widgets and make assertions about page content. 6 | # 7 | # If you use these step definitions as basis for your features you will quickly end up 8 | # with features that are: 9 | # 10 | # * Hard to maintain 11 | # * Verbose to read 12 | # 13 | # A much better approach is to write your own higher level step definitions, following 14 | # the advice in the following blog posts: 15 | # 16 | # * http://benmabey.com/2008/05/19/imperative-vs-declarative-scenarios-in-user-stories.html 17 | # * http://dannorth.net/2011/01/31/whose-domain-is-it-anyway/ 18 | # * http://elabs.se/blog/15-you-re-cuking-it-wrong 19 | # 20 | 21 | 22 | require 'uri' 23 | require 'cgi' 24 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths")) 25 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "selectors")) 26 | 27 | module WithinHelpers 28 | def with_scope(locator) 29 | locator ? within(*selector_for(locator)) { yield } : yield 30 | end 31 | end 32 | World(WithinHelpers) 33 | 34 | # Single-line step scoper 35 | When /^(.*) within (.*[^:])$/ do |step, parent| 36 | with_scope(parent) { When step } 37 | end 38 | 39 | # Multi-line step scoper 40 | When /^(.*) within (.*[^:]):$/ do |step, parent, table_or_string| 41 | with_scope(parent) { When "#{step}:", table_or_string } 42 | end 43 | 44 | Given /^(?:|I )am on (.+)$/ do |page_name| 45 | visit path_to(page_name) 46 | end 47 | 48 | When /^(?:|I )go to (.+)$/ do |page_name| 49 | visit path_to(page_name) 50 | end 51 | 52 | When /^(?:|I )press "([^"]*)"$/ do |button| 53 | click_button(button) 54 | end 55 | 56 | When /^(?:|I )follow "([^"]*)"$/ do |link| 57 | click_link(link) 58 | end 59 | 60 | When /^(?:|I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value| 61 | fill_in(field, :with => value) 62 | end 63 | 64 | When /^(?:|I )fill in "([^"]*)" for "([^"]*)"$/ do |value, field| 65 | fill_in(field, :with => value) 66 | end 67 | 68 | # Use this to fill in an entire form with data from a table. Example: 69 | # 70 | # When I fill in the following: 71 | # | Account Number | 5002 | 72 | # | Expiry date | 2009-11-01 | 73 | # | Note | Nice guy | 74 | # | Wants Email? | | 75 | # 76 | # TODO: Add support for checkbox, select og option 77 | # based on naming conventions. 78 | # 79 | When /^(?:|I )fill in the following:$/ do |fields| 80 | fields.rows_hash.each do |name, value| 81 | When %{I fill in "#{name}" with "#{value}"} 82 | end 83 | end 84 | 85 | When /^(?:|I )select "([^"]*)" from "([^"]*)"$/ do |value, field| 86 | select(value, :from => field) 87 | end 88 | 89 | When /^(?:|I )check "([^"]*)"$/ do |field| 90 | check(field) 91 | end 92 | 93 | When /^(?:|I )uncheck "([^"]*)"$/ do |field| 94 | uncheck(field) 95 | end 96 | 97 | When /^(?:|I )choose "([^"]*)"$/ do |field| 98 | choose(field) 99 | end 100 | 101 | When /^(?:|I )attach the file "([^"]*)" to "([^"]*)"$/ do |path, field| 102 | attach_file(field, File.expand_path(path)) 103 | end 104 | 105 | Then /^(?:|I )should (not )?see "([^"]*)"$/ do |negate, text| 106 | should_name = negate ? :should_not : :should 107 | if page.respond_to? should_name 108 | page.send should_name, have_content(text) 109 | else 110 | assert(negate ? page.has_no_content?(text) : page.has_content?(text)) 111 | end 112 | end 113 | 114 | Then /^(?:|I )should (not )?see \/([^\/]*)\/$/ do |negate, regexp| 115 | regexp = Regexp.new(regexp) 116 | should_name = negate ? :should_not : :should 117 | if page.respond_to? should_name 118 | page.send should_name, have_xpath('//*', :text => regexp) 119 | else 120 | assert(negate ? page.has_no_xpath?('//*', :text => regexp) : page.has_xpath?('//*', :text => regexp)) 121 | end 122 | end 123 | 124 | Then /^(?:|I )should be on (.+)$/ do |page_name| 125 | current_path = URI.parse(current_url).path 126 | expect(current_path).to eq path_to(page_name) 127 | end 128 | 129 | Then /^(?:|I )should have the following query string:$/ do |expected_pairs| 130 | query = URI.parse(current_url).query 131 | actual_params = query ? CGI.parse(query) : {} 132 | expected_params = {} 133 | expected_pairs.rows_hash.each_pair{|k,v| expected_params[k] = v.split(',')} 134 | 135 | expect(actual_params).to eq expected_params 136 | end 137 | 138 | Then /^show me the page$/ do 139 | save_and_open_page 140 | end 141 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | #require 'bundler' 2 | begin 3 | Bundler.setup(:default, :development) 4 | rescue Bundler::BundlerError => e 5 | $stderr.puts e.message 6 | $stderr.puts "Run `bundle install` to install missing gems" 7 | exit e.status_code 8 | end 9 | 10 | require 'aruba/cucumber' 11 | require 'capybara/cucumber' 12 | 13 | Before do 14 | @aruba_timeout_seconds = 120 15 | end 16 | -------------------------------------------------------------------------------- /features/support/paths.rb: -------------------------------------------------------------------------------- 1 | module NavigationHelpers 2 | # Maps a name to a path. Used by the 3 | # 4 | # When /^I go to (.+)$/ do |page_name| 5 | # 6 | # step definition in web_steps.rb 7 | # 8 | def path_to(page_name) 9 | case page_name 10 | 11 | when /the home\s?page/ 12 | '/' 13 | when /the new user page/ 14 | '/users/new' 15 | else 16 | begin 17 | page_name =~ /the (.*) page/ 18 | path_components = $1.split(/\s+/) 19 | self.send(path_components.push('path').join('_').to_sym) 20 | rescue Object => e 21 | raise "Can't find mapping from \"#{page_name}\" to a path.\n" + 22 | "Now, go and add a mapping in #{__FILE__}" 23 | end 24 | end 25 | end 26 | end 27 | 28 | World(NavigationHelpers) 29 | -------------------------------------------------------------------------------- /features/support/rails.rb: -------------------------------------------------------------------------------- 1 | PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze 2 | APP_NAME = 'testapp'.freeze 3 | BUNDLE_ENV_VARS = %w(RUBYOPT BUNDLE_PATH BUNDLE_BIN_PATH BUNDLE_GEMFILE) 4 | ORIGINAL_BUNDLE_VARS = Hash[ENV.select{ |key,value| BUNDLE_ENV_VARS.include?(key) }] 5 | 6 | ENV['RAILS_ENV'] = 'test' 7 | 8 | Before do 9 | ENV['BUNDLE_GEMFILE'] = File.join(Dir.pwd, ENV['BUNDLE_GEMFILE']) unless ENV['BUNDLE_GEMFILE'].start_with?(Dir.pwd) 10 | @framework_version = nil 11 | end 12 | 13 | After do 14 | ORIGINAL_BUNDLE_VARS.each_pair do |key, value| 15 | ENV[key] = value 16 | end 17 | end 18 | 19 | When /^I reset Bundler environment variable$/ do 20 | BUNDLE_ENV_VARS.each do |key| 21 | ENV[key] = nil 22 | end 23 | end 24 | 25 | module RailsCommandHelpers 26 | def framework_version?(version_string) 27 | framework_version =~ /^#{version_string}/ 28 | end 29 | 30 | def framework_version 31 | @framework_version ||= `rails -v`[/^Rails (.+)$/, 1] 32 | end 33 | 34 | def framework_major_version 35 | framework_version.split(".").first.to_i 36 | end 37 | 38 | def new_application_command(app_name) 39 | framework_major_version >= 3 ? "rails new #{app_name} --skip-sprockets --skip-javascript --skip-bundle" : "rails #{app_name}" 40 | end 41 | 42 | def generator_command 43 | framework_major_version >= 3 ? "rails generate" : "script/generate" 44 | end 45 | 46 | def runner_command 47 | framework_major_version >= 3 ? "rails runner" : "script/runner" 48 | end 49 | 50 | end 51 | 52 | World(RailsCommandHelpers) 53 | -------------------------------------------------------------------------------- /features/support/selectors.rb: -------------------------------------------------------------------------------- 1 | module HtmlSelectorsHelpers 2 | # Maps a name to a selector. Used primarily by the 3 | # 4 | # When /^(.+) within (.+)$/ do |step, scope| 5 | # 6 | # step definitions in web_steps.rb 7 | # 8 | def selector_for(locator) 9 | case locator 10 | when "the page" 11 | "html > body" 12 | else 13 | raise "Can't find mapping from \"#{locator}\" to a selector.\n" + 14 | "Now, go and add a mapping in #{__FILE__}" 15 | end 16 | end 17 | end 18 | 19 | World(HtmlSelectorsHelpers) 20 | -------------------------------------------------------------------------------- /gemfiles/rails30.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mime-types", "< 2.0", :platform => :ruby_18 6 | gem "rubyzip", "< 1.0", :platform => :ruby_18 7 | gem "rails", "~> 3.0.0" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails32.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mime-types", "< 2.0", :platform => :ruby_18 6 | gem "rubyzip", "< 1.0", :platform => :ruby_18 7 | gem "rails", "~> 3.2.0" 8 | gem "jquery-rails" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails40.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mime-types", "< 2.0", :platform => :ruby_18 6 | gem "rubyzip", "< 1.0", :platform => :ruby_18 7 | gem "rails", "~> 4.0.0" 8 | gem "jquery-rails" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails41.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mime-types", "< 2.0", :platform => :ruby_18 6 | gem "rubyzip", "< 1.0", :platform => :ruby_18 7 | gem "rails", "~> 4.1.0" 8 | gem "jquery-rails" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails42.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mime-types", "< 2.0", :platform => :ruby_18 6 | gem "rubyzip", "< 1.0", :platform => :ruby_18 7 | gem "rails", "~> 4.2.0" 8 | gem "jquery-rails" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | # For use by Railed and de-Railed applications alike 2 | 3 | require 'active_record' 4 | require File.join(File.dirname(__FILE__), 'lib', 'activerecord-tableless') 5 | -------------------------------------------------------------------------------- /lib/activerecord-tableless.rb: -------------------------------------------------------------------------------- 1 | # See #ActiveRecord::Tableless 2 | require 'activerecord-tableless/version' 3 | 4 | module ActiveRecord 5 | 6 | # = ActiveRecord::Tableless 7 | # 8 | # Allow classes to behave like ActiveRecord models, but without an associated 9 | # database table. A great way to capitalize on validations. Based on the 10 | # original post at http://www.railsweenie.com/forums/2/topics/724 (which seems 11 | # to have disappeared from the face of the earth). 12 | # 13 | # = Example usage 14 | # 15 | # class ContactMessage < ActiveRecord::Base 16 | # 17 | # has_no_table 18 | # 19 | # column :name, :string 20 | # column :email, :string 21 | # column :message, :string 22 | # 23 | # end 24 | # 25 | # msg = ContactMessage.new( params[:msg] ) 26 | # if msg.valid? 27 | # ContactMessageSender.deliver_message( msg ) 28 | # redirect_to :action => :sent 29 | # end 30 | # 31 | module Tableless 32 | require 'active_record' 33 | 34 | class NoDatabase < StandardError; end 35 | class Unsupported < StandardError; end 36 | 37 | def self.included( base ) #:nodoc: 38 | base.send :extend, ActsMethods 39 | end 40 | 41 | module ActsMethods #:nodoc: 42 | 43 | # A model that needs to be tableless will call this method to indicate 44 | # it. 45 | def has_no_table(options = {:database => :fail_fast}) 46 | raise ArgumentError.new("Invalid database option '#{options[:database]}'") unless [:fail_fast, :pretend_success].member? options[:database] 47 | # keep our options handy 48 | if ActiveRecord::VERSION::STRING < "3.1.0" 49 | write_inheritable_attribute(:tableless_options, 50 | { :database => options[:database], 51 | :columns => [] 52 | } 53 | ) 54 | class_inheritable_reader :tableless_options 55 | elsif ActiveRecord::VERSION::STRING >= "3.2.0" 56 | class_attribute :tableless_options 57 | self.tableless_options = { 58 | :database => options[:database], 59 | :columns => [] 60 | } 61 | else 62 | raise Unsupported.new("Sorry, ActiveRecord version #{ActiveRecord::VERSION::STRING} is not supported") 63 | end 64 | 65 | # extend 66 | extend ActiveRecord::Tableless::SingletonMethods 67 | extend ActiveRecord::Tableless::ClassMethods 68 | 69 | # include 70 | include ActiveRecord::Tableless::InstanceMethods 71 | 72 | # setup columns 73 | end 74 | 75 | def tableless? 76 | false 77 | end 78 | 79 | end 80 | 81 | module SingletonMethods 82 | 83 | # Return the list of columns registered for the model. Used internally by 84 | # ActiveRecord 85 | def columns 86 | tableless_options[:columns] 87 | end 88 | 89 | # Register a new column. 90 | 91 | if ActiveRecord::VERSION::STRING >= "4.2.0" 92 | def column(name, sql_type = nil, default = nil, null = true) 93 | cast_type = "ActiveRecord::Type::#{sql_type.to_s.camelize}".constantize.new 94 | tableless_options[:columns] << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, cast_type, sql_type.to_s, null) 95 | end 96 | else 97 | def column(name, sql_type = nil, default = nil, null = true) 98 | tableless_options[:columns] << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null) 99 | end 100 | end 101 | 102 | # Register a set of columns with the same SQL type 103 | def add_columns(sql_type, *args) 104 | args.each do |col| 105 | column col, sql_type 106 | end 107 | end 108 | 109 | def destroy(*args) 110 | case tableless_options[:database] 111 | when :pretend_success 112 | self.new() 113 | when :fail_fast 114 | raise NoDatabase.new("Can't #destroy on Tableless class") 115 | end 116 | end 117 | 118 | def destroy_all(*args) 119 | case tableless_options[:database] 120 | when :pretend_success 121 | [] 122 | when :fail_fast 123 | raise NoDatabase.new("Can't #destroy_all on Tableless class") 124 | end 125 | end 126 | 127 | case ActiveRecord::VERSION::MAJOR 128 | when 3 129 | def all(*args) 130 | case tableless_options[:database] 131 | when :pretend_success 132 | [] 133 | when :fail_fast 134 | raise NoDatabase.new("Can't #find_every on Tableless class") 135 | end 136 | 137 | end 138 | when 4 139 | def find_by_sql(*args) 140 | case tableless_options[:database] 141 | when :pretend_success 142 | [] 143 | when :fail_fast 144 | raise NoDatabase.new("Can't #find_by_sql on Tableless class") 145 | end 146 | 147 | end 148 | else 149 | raise Unsupported.new("Unsupported ActiveRecord version") 150 | end 151 | 152 | def transaction(&block) 153 | # case tableless_options[:database] 154 | # when :pretend_success 155 | @_current_transaction_records ||= [] 156 | yield 157 | # when :fail_fast 158 | # raise NoDatabase.new("Can't #transaction on Tableless class") 159 | # end 160 | end 161 | 162 | def tableless? 163 | true 164 | end 165 | 166 | def table_exists? 167 | false 168 | end 169 | end 170 | 171 | module ClassMethods 172 | 173 | def from_query_string(query_string) 174 | unless query_string.blank? 175 | params = query_string.split('&').collect do |chunk| 176 | next if chunk.empty? 177 | key, value = chunk.split('=', 2) 178 | next if key.empty? 179 | value = value.nil? ? nil : CGI.unescape(value) 180 | [ CGI.unescape(key), value ] 181 | end.compact.to_h 182 | 183 | new(params) 184 | else 185 | new 186 | end 187 | end 188 | 189 | def connection 190 | conn = Object.new() 191 | def conn.quote_table_name(*args) 192 | "" 193 | end 194 | def conn.quote_column_name(*args) 195 | "" 196 | end 197 | def conn.substitute_at(*args) 198 | nil 199 | end 200 | def conn.schema_cache(*args) 201 | schema_cache = Object.new() 202 | def schema_cache.columns_hash(*args) 203 | Hash.new() 204 | end 205 | schema_cache 206 | end 207 | conn 208 | end 209 | 210 | end 211 | 212 | module InstanceMethods 213 | 214 | def to_query_string(prefix = nil) 215 | attributes.to_a.collect{|(name,value)| escaped_var_name(name, prefix) + "=" + escape_for_url(value) if value }.compact.join("&") 216 | end 217 | 218 | def quote_value(value, column = nil) 219 | "" 220 | end 221 | 222 | %w(create create_record _create_record update update_record _update_record).each do |method_name| 223 | define_method(method_name) do |*args| 224 | case self.class.tableless_options[:database] 225 | when :pretend_success 226 | true 227 | when :fail_fast 228 | raise NoDatabase.new("Can't ##{method_name} a Tableless object") 229 | end 230 | end 231 | end 232 | 233 | def destroy 234 | case self.class.tableless_options[:database] 235 | when :pretend_success 236 | @destroyed = true 237 | freeze 238 | when :fail_fast 239 | raise NoDatabase.new("Can't #destroy a Tableless object") 240 | end 241 | end 242 | 243 | def reload(*args) 244 | case self.class.tableless_options[:database] 245 | when :pretend_success 246 | self 247 | when :fail_fast 248 | raise NoDatabase.new("Can't #reload a Tableless object") 249 | end 250 | end 251 | 252 | if ActiveRecord::VERSION::MAJOR >= 3 253 | def add_to_transaction 254 | end 255 | end 256 | 257 | private 258 | 259 | def escaped_var_name(name, prefix = nil) 260 | prefix ? "#{URI.escape(prefix)}[#{URI.escape(name)}]" : URI.escape(name) 261 | end 262 | 263 | def escape_for_url(value) 264 | case value 265 | when true then "1" 266 | when false then "0" 267 | when nil then "" 268 | else URI.escape(value.to_s) 269 | end 270 | rescue 271 | "" 272 | end 273 | 274 | end 275 | 276 | end 277 | end 278 | 279 | ActiveRecord::Base.send( :include, ActiveRecord::Tableless ) 280 | -------------------------------------------------------------------------------- /lib/activerecord-tableless/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Tableless 3 | unless defined?(ActiveRecord::Tableless::VERSION) 4 | VERSION = "2.0.0".freeze 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/lib/activerecord-tableless_spec.rb: -------------------------------------------------------------------------------- 1 | require 'sqlite3' 2 | require 'active_record' 3 | require 'activerecord-tableless' 4 | require 'logger' 5 | require 'spec_helper' 6 | 7 | def make_tableless_model(database = nil, nested = nil) 8 | eval < :#{database}" : 'has_no_table'} 11 | column :id, :integer 12 | column :name, :string 13 | #{if nested 14 | ' 15 | has_many :arm_rests 16 | accepts_nested_attributes_for :arm_rests 17 | ' 18 | end} 19 | end 20 | EOCLASS 21 | if nested 22 | eval < :#{database}" : 'has_no_table'} 25 | belongs_to :chair 26 | column :id, :integer 27 | column :chair_id, :integer 28 | column :name, :string 29 | end 30 | EOCLASS 31 | end 32 | end 33 | 34 | def remove_models 35 | Object.send(:remove_const, :Chair) rescue nil 36 | Object.send(:remove_const, :ArmRest) rescue nil 37 | end 38 | 39 | ActiveRecord::Base.logger = Logger.new(STDERR) 40 | ActiveRecord::Base.logger.level = Logger::Severity::UNKNOWN 41 | 42 | shared_examples_for "an active record instance" do 43 | it { should respond_to :id } 44 | it { should respond_to :id= } 45 | it { should respond_to :name } 46 | it { should respond_to :name= } 47 | it { should respond_to :update_attributes } 48 | describe "#attributes=" do 49 | before(:example){ subject.attributes=({:name => 'Jarl Friis'}) } 50 | it "assign attributes" do 51 | expect(subject.name).to eq 'Jarl Friis' 52 | end 53 | end 54 | end 55 | 56 | shared_examples_for "a nested active record" do 57 | describe "conllection#build" do 58 | specify do 59 | expect(subject.arm_rests.build({:name => 'nice arm_rest'})).to be_an_instance_of(ArmRest) 60 | end 61 | end 62 | describe "conllection#<<" do 63 | specify do 64 | expect(subject.arm_rests << ArmRest.new({:name => 'nice arm_rest'})).to have(1).items 65 | end 66 | describe 'appending two children' do 67 | before(:example) do 68 | subject.arm_rests << [ArmRest.new({:name => 'left'}), 69 | ArmRest.new({:name => 'right'})] 70 | end 71 | it "assigns nested attributes" do 72 | expect(subject.arm_rests[0].name).to eq 'left' 73 | expect(subject.arm_rests[1].name).to eq 'right' 74 | end 75 | end 76 | end 77 | describe "#attributes=" do 78 | before(:example){ subject.attributes=({ :name => 'Jarl Friis', 79 | :arm_rests_attributes => [ 80 | {:name => 'left'}, 81 | {:name => 'right'} 82 | ] 83 | }) } 84 | it "assigns attributes" do 85 | expect(subject.name).to eq 'Jarl Friis' 86 | end 87 | it "assigns nested attributes" do 88 | expect(subject.arm_rests[0].name).to eq 'left' 89 | expect(subject.arm_rests[1].name).to eq 'right' 90 | end 91 | end 92 | end 93 | 94 | shared_examples_for "a tableless model with fail_fast" do 95 | case ActiveRecord::VERSION::MAJOR 96 | when 3 97 | describe "#all" do 98 | it "raises ActiveRecord::Tableless::NoDatabase" do 99 | expect { subject.all }.to raise_exception(ActiveRecord::Tableless::NoDatabase) 100 | end 101 | end 102 | when 4 103 | describe "#all" do 104 | it "raises ActiveRecord::Tableless::NoDatabase" do 105 | expect { subject.all }.to_not raise_exception 106 | end 107 | end 108 | describe "#all[]" do 109 | it "raises ActiveRecord::Tableless::NoDatabase" do 110 | expect { subject.all[0] }.to raise_exception(ActiveRecord::Tableless::NoDatabase) 111 | end 112 | end 113 | end 114 | describe "#create" do 115 | it "raises ActiveRecord::Tableless::NoDatabase" do 116 | expect { subject.create(:name => 'Jarl') }.to raise_exception(ActiveRecord::Tableless::NoDatabase) 117 | end 118 | end 119 | describe "#destroy" do 120 | it "raises ActiveRecord::Tableless::NoDatabase" do 121 | expect { subject.destroy(1) }.to raise_exception(ActiveRecord::Tableless::NoDatabase) 122 | end 123 | end 124 | describe "#destroy_all" do 125 | it "raises ActiveRecord::Tableless::NoDatabase" do 126 | expect { subject.destroy_all }.to raise_exception(ActiveRecord::Tableless::NoDatabase) 127 | end 128 | end 129 | end 130 | 131 | shared_examples_for "a tableless model instance with fail_fast" do 132 | it_behaves_like "an active record instance" 133 | describe "#save" do 134 | it "raises ActiveRecord::Tableless::NoDatabase" do 135 | expect { subject.save }.to raise_exception(ActiveRecord::Tableless::NoDatabase) 136 | end 137 | end 138 | describe "#save!" do 139 | it "raises ActiveRecord::Tableless::NoDatabase" do 140 | expect { subject.save! }.to raise_exception(ActiveRecord::Tableless::NoDatabase) 141 | end 142 | end 143 | describe "#reload" do 144 | it "raises ActiveRecord::Tableless::NoDatabase" do 145 | expect { subject.reload }.to raise_exception(ActiveRecord::Tableless::NoDatabase) 146 | end 147 | end 148 | describe "#update_attributes" do 149 | it "raises ActiveRecord::Tableless::NoDatabase" do 150 | expect { subject.update_attributes(:name => 'Jarl') }.to raise_exception(StandardError) 151 | end 152 | end 153 | end 154 | 155 | describe "Tableless model with fail_fast" do 156 | before(:context) {make_tableless_model(nil, nil)} 157 | after(:context){ remove_models } 158 | subject { Chair } 159 | it_behaves_like "a tableless model with fail_fast" 160 | describe "instance" do 161 | subject { Chair.new(:name => 'Jarl') } 162 | it_behaves_like "a tableless model instance with fail_fast" 163 | end 164 | end 165 | 166 | describe "Tableless nested with fail_fast" do 167 | before(:context) {make_tableless_model(nil, true)} 168 | after(:context){ remove_models } 169 | subject { Chair } 170 | it_behaves_like "a tableless model with fail_fast" 171 | describe "#new" do 172 | it "accepts attributes" do 173 | expect(subject.new(:name => "Jarl")).to be_an_instance_of(subject) 174 | end 175 | it "assign attributes" do 176 | expect(subject.new(:name => "Jarl").name).to eq "Jarl" 177 | end 178 | end 179 | describe "instance" do 180 | subject { Chair.new(:name => 'Jarl') } 181 | it_behaves_like "a tableless model instance with fail_fast" 182 | it_behaves_like "a nested active record" 183 | describe "#update_attributes" do 184 | it "raises ActiveRecord::Tableless::NoDatabase" do 185 | expect do 186 | subject.update_attributes(:arm_rests => {:name => 'nice arm_rest'}) 187 | end.to raise_exception(StandardError) 188 | end 189 | end 190 | end 191 | describe "instance with nested models" do 192 | subject do 193 | Chair.new(:name => "Jarl", 194 | :arm_rests => [ 195 | ArmRest.new(:name => 'left'), 196 | ArmRest.new(:name => 'right'), 197 | ]) 198 | end 199 | it {should be_an_instance_of(Chair) } 200 | it {should have(2).arm_rests } 201 | end 202 | describe "instance with nested attributes" do 203 | subject do 204 | Chair.new(:name => "Jarl", 205 | :arm_rests_attributes => [ 206 | {:name => 'left'}, 207 | {:name => 'right'}, 208 | ]) 209 | end 210 | it {should be_an_instance_of(Chair)} 211 | it {should have(2).arm_rests } 212 | end 213 | end 214 | 215 | ## 216 | ## Succeeding database 217 | ## 218 | shared_examples_for "a model with succeeding database" do 219 | describe "#all" do 220 | specify { expect(subject.all).to eq []} 221 | end 222 | describe "#create" do 223 | specify { expect(subject.create(:name => 'Jarl')).to be_an_instance_of(subject) } 224 | end 225 | describe "#destroy" do 226 | specify { expect(subject.destroy(1)).to be_an_instance_of(subject) } 227 | end 228 | describe "#destroy_all" do 229 | specify { expect(subject.destroy_all).to eq [] } 230 | end 231 | end 232 | 233 | shared_examples_for "an instance with succeeding database" do 234 | it_behaves_like "an active record instance" 235 | 236 | describe "#save" do 237 | specify { expect(subject.save).to eq true } 238 | end 239 | describe "#save!" do 240 | specify { expect(subject.save!).to eq true } 241 | end 242 | describe "#reload" do 243 | before { subject.save! } 244 | specify { expect(subject.reload).to eq subject } 245 | end 246 | describe "#update_attributes" do 247 | specify { expect(subject.update_attributes(:name => 'Jarl Friis')).to eq true } 248 | end 249 | end 250 | 251 | describe "ActiveRecord with real database" do 252 | ##This is only here to ensure that the shared examples are actually behaving like a real database. 253 | before(:context) do 254 | FileUtils.mkdir_p "tmp" 255 | ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => 'tmp/test.db') 256 | ActiveRecord::Base.connection.execute("drop table if exists chairs") 257 | ActiveRecord::Base.connection.execute("create table chairs (id INTEGER PRIMARY KEY, name TEXT )") 258 | 259 | class Chair < ActiveRecord::Base 260 | end 261 | end 262 | after(:context) do 263 | remove_models 264 | ActiveRecord::Base.clear_all_connections! 265 | end 266 | 267 | subject { Chair } 268 | it_behaves_like "a model with succeeding database" 269 | describe "instance" do 270 | subject { Chair.new(:name => 'Jarl') } 271 | it_behaves_like "an instance with succeeding database" 272 | end 273 | end 274 | 275 | describe "Tableless model with succeeding database" do 276 | before(:context) { make_tableless_model(:pretend_success, nil) } 277 | after(:context){ remove_models } 278 | subject { Chair } 279 | it_behaves_like "a model with succeeding database" 280 | describe "instance" do 281 | subject { Chair.new(:name => 'Jarl') } 282 | it_behaves_like "an instance with succeeding database" 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/collection_matchers' 2 | --------------------------------------------------------------------------------