├── .gitignore ├── .rspec ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app └── controllers │ └── reactive_record │ ├── application_controller.rb │ └── reactive_record_controller.rb ├── config └── routes.rb ├── lib ├── Gemfile ├── reactive-record.rb └── reactive_record │ ├── active_record │ ├── aggregations.rb │ ├── associations.rb │ ├── base.rb │ ├── class_methods.rb │ ├── error.rb │ ├── instance_methods.rb │ └── reactive_record │ │ ├── base.rb │ │ ├── collection.rb │ │ ├── isomorphic_base.rb │ │ └── while_loading.rb │ ├── engine.rb │ ├── interval.rb │ ├── permissions.rb │ ├── pry.rb │ ├── reactive_scope.rb │ ├── serializers.rb │ ├── server_data_cache.rb │ └── version.rb ├── reactive-record.gemspec ├── script └── rails └── spec ├── server_unit_tests └── pry_rescue_spec.rb └── test_app ├── Gemfile ├── Gemfile.lock ├── README.rdoc ├── Rakefile ├── app ├── assets │ ├── javascripts │ │ ├── application.rb │ │ ├── components │ │ │ ├── another_component.rb │ │ │ ├── empty_component.rb │ │ │ ├── todo_item_component.js.rb │ │ │ ├── todos_component.js.rb │ │ │ └── todos_main_component.rb │ │ ├── react_js_test_only.js │ │ ├── reactive_record_config.js │ │ └── spec │ │ │ └── reactive_record_xspec.js.rb │ └── stylesheets │ │ └── application.css ├── controllers │ ├── application_controller.rb │ ├── home_controller.rb │ └── test_controller.rb ├── helpers │ └── application_helper.rb ├── mailers │ └── .gitkeep ├── models │ └── .gitkeep └── views │ ├── components.rb │ ├── components │ └── test.rb │ ├── home │ └── index.html.erb │ ├── layouts │ └── application.html.erb │ ├── models.rb │ └── models │ ├── address.rb │ ├── comment.rb │ ├── todo_item.rb │ └── user.rb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── secret_token.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml └── routes.rb ├── db ├── development.sqlite3 ├── migrate │ ├── 20150617002932_create_todo_items.rb │ ├── 20150617134028_create_users.rb │ ├── 20150729195556_add_address_to_user.rb │ ├── 20150826142045_create_comments.rb │ ├── 20150828172008_add_single_comment_to_todo_item.rb │ ├── 20150908184118_add_address_id_to_user.rb │ ├── 20150917220236_add_second_address_to_user.rb │ ├── 20151009000111_add_test_data_attributes_to_user.rb │ └── 20160129182544_add_test_enum_to_user.rb ├── schema.rb ├── seeds.rb └── test.sqlite3 ├── log ├── .gitkeep └── test.log ├── public ├── 404.html ├── 422.html ├── 500.html └── favicon.ico ├── script └── rails ├── spec-opal ├── active_record │ ├── aggregations_spec.rb │ ├── associations_spec.rb │ ├── base_spec.rb │ ├── dummy_value_spec.rb │ ├── edge_cases_spec.rb │ ├── enum_spec.rb │ ├── instance_methods_spec.rb │ ├── many_to_many_spec.rb │ ├── non_ar_aggregations_spec.rb │ ├── permissions_spec.rb │ ├── prerendering_spec.rb │ ├── reactive_record_load_spec.rb │ ├── rendering_spec.rb │ ├── revert_record_spec.rb │ ├── save_spec.rb │ ├── scope_spec.rb │ ├── update_aggregations_spec.rb │ ├── update_associations_spec.rb │ ├── update_attributes_spec.rb │ ├── update_scopes_spec.rb │ └── virtual_methods_spec.rb ├── index.html.erb ├── spec_helper.js.rb └── vendor │ └── es5-shim.min.js ├── spec ├── server_unit_tests │ └── pry_rescue_spec.rb └── spec_helper.rb └── tmp └── react-rails ├── JSXTransformer.js └── react.js /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/test_app/db/*.sqlite3 5 | spec/test_app/log/*.log 6 | spec/test_app/tmp/ 7 | spec/test_app/.sass-cache 8 | .DS_Store -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | 3 | # gem 'reactive-ruby', File.exist?("../reactive-ruby") ? {path: "../reactive-ruby"} : "reactive-ruby" 4 | # gem 'opal' #, git: "https://github.com/catprintlabs/opal.git" 5 | # gem 'opal-browser' 6 | # gem 'react-rails', File.exist?("../react-rails") ? {path: "../react-rails"} : {git: "https://github.com/catprintlabs/react-rails.git", :branch => 'isomorphic-methods-support'} 7 | #gem 'reactrb', path: "../reactive-ruby", branch: "0-8-stable" 8 | #gem 'opal-rails', git: "https://github.com/opal/opal-rails.git" 9 | gemspec 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | reactive-record (0.7.43) 5 | opal-browser 6 | opal-rails 7 | rails (>= 3.2.13) 8 | react-rails 9 | reactrb 10 | therubyracer 11 | 12 | PATH 13 | remote: ../reactive-ruby 14 | specs: 15 | reactrb (0.7.42) 16 | opal (>= 0.8.0) 17 | opal-activesupport (>= 0.2.0) 18 | opal-browser (= 0.2.0) 19 | 20 | GEM 21 | specs: 22 | actionmailer (4.2.6) 23 | actionpack (= 4.2.6) 24 | actionview (= 4.2.6) 25 | activejob (= 4.2.6) 26 | mail (~> 2.5, >= 2.5.4) 27 | rails-dom-testing (~> 1.0, >= 1.0.5) 28 | actionpack (4.2.6) 29 | actionview (= 4.2.6) 30 | activesupport (= 4.2.6) 31 | rack (~> 1.6) 32 | rack-test (~> 0.6.2) 33 | rails-dom-testing (~> 1.0, >= 1.0.5) 34 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 35 | actionview (4.2.6) 36 | activesupport (= 4.2.6) 37 | builder (~> 3.1) 38 | erubis (~> 2.7.0) 39 | rails-dom-testing (~> 1.0, >= 1.0.5) 40 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 41 | activejob (4.2.6) 42 | activesupport (= 4.2.6) 43 | globalid (>= 0.3.0) 44 | activemodel (4.2.6) 45 | activesupport (= 4.2.6) 46 | builder (~> 3.1) 47 | activerecord (4.2.6) 48 | activemodel (= 4.2.6) 49 | activesupport (= 4.2.6) 50 | arel (~> 6.0) 51 | activesupport (4.2.6) 52 | i18n (~> 0.7) 53 | json (~> 1.7, >= 1.7.7) 54 | minitest (~> 5.1) 55 | thread_safe (~> 0.3, >= 0.3.4) 56 | tzinfo (~> 1.1) 57 | arel (6.0.3) 58 | babel-source (5.8.35) 59 | babel-transpiler (0.7.0) 60 | babel-source (>= 4.0, < 6) 61 | execjs (~> 2.0) 62 | builder (3.2.2) 63 | coderay (1.1.1) 64 | coffee-script-source (1.10.0) 65 | concurrent-ruby (1.0.2) 66 | connection_pool (2.2.0) 67 | diff-lcs (1.2.5) 68 | erubis (2.7.0) 69 | execjs (2.7.0) 70 | globalid (0.3.6) 71 | activesupport (>= 4.1.0) 72 | hike (1.2.3) 73 | i18n (0.7.0) 74 | jquery-rails (4.1.1) 75 | rails-dom-testing (>= 1, < 3) 76 | railties (>= 4.2.0) 77 | thor (>= 0.14, < 2.0) 78 | json (1.8.3) 79 | libv8 (3.16.14.15) 80 | loofah (2.0.3) 81 | nokogiri (>= 1.5.9) 82 | mail (2.6.4) 83 | mime-types (>= 1.16, < 4) 84 | method_source (0.8.2) 85 | mime-types (3.1) 86 | mime-types-data (~> 3.2015) 87 | mime-types-data (3.2016.0521) 88 | mini_portile2 (2.0.0) 89 | minitest (5.9.0) 90 | nokogiri (1.6.7.2) 91 | mini_portile2 (~> 2.0.0.rc2) 92 | opal (0.8.1) 93 | hike (~> 1.2) 94 | sourcemap (~> 0.1.0) 95 | sprockets (~> 3.1) 96 | tilt (>= 1.4) 97 | opal-activesupport (0.3.0) 98 | opal (>= 0.5.0, < 1.0.0) 99 | opal-browser (0.2.0) 100 | opal 101 | paggio 102 | opal-jquery (0.4.1) 103 | opal (>= 0.7.0, < 0.10.0) 104 | opal-rails (0.8.1) 105 | jquery-rails 106 | opal (~> 0.8.0) 107 | opal-activesupport (>= 0.0.5) 108 | opal-jquery (~> 0.4.0) 109 | opal-rspec (~> 0.4.3) 110 | rails (>= 3.2, < 5.0) 111 | sprockets-rails (< 3.0) 112 | opal-rspec (0.4.3) 113 | opal (>= 0.7.0, < 0.9) 114 | paggio (0.2.6) 115 | pry (0.10.3) 116 | coderay (~> 1.1.0) 117 | method_source (~> 0.8.1) 118 | slop (~> 3.4) 119 | rack (1.6.4) 120 | rack-test (0.6.3) 121 | rack (>= 1.0) 122 | rails (4.2.6) 123 | actionmailer (= 4.2.6) 124 | actionpack (= 4.2.6) 125 | actionview (= 4.2.6) 126 | activejob (= 4.2.6) 127 | activemodel (= 4.2.6) 128 | activerecord (= 4.2.6) 129 | activesupport (= 4.2.6) 130 | bundler (>= 1.3.0, < 2.0) 131 | railties (= 4.2.6) 132 | sprockets-rails 133 | rails-deprecated_sanitizer (1.0.3) 134 | activesupport (>= 4.2.0.alpha) 135 | rails-dom-testing (1.0.7) 136 | activesupport (>= 4.2.0.beta, < 5.0) 137 | nokogiri (~> 1.6.0) 138 | rails-deprecated_sanitizer (>= 1.0.1) 139 | rails-html-sanitizer (1.0.3) 140 | loofah (~> 2.0) 141 | railties (4.2.6) 142 | actionpack (= 4.2.6) 143 | activesupport (= 4.2.6) 144 | rake (>= 0.8.7) 145 | thor (>= 0.18.1, < 2.0) 146 | rake (11.1.2) 147 | react-rails (1.7.1) 148 | babel-transpiler (>= 0.7.0) 149 | coffee-script-source (~> 1.8) 150 | connection_pool 151 | execjs 152 | rails (>= 3.2) 153 | tilt 154 | ref (2.0.0) 155 | rspec-core (3.4.4) 156 | rspec-support (~> 3.4.0) 157 | rspec-expectations (3.4.0) 158 | diff-lcs (>= 1.2.0, < 2.0) 159 | rspec-support (~> 3.4.0) 160 | rspec-mocks (3.4.1) 161 | diff-lcs (>= 1.2.0, < 2.0) 162 | rspec-support (~> 3.4.0) 163 | rspec-rails (3.4.2) 164 | actionpack (>= 3.0, < 4.3) 165 | activesupport (>= 3.0, < 4.3) 166 | railties (>= 3.0, < 4.3) 167 | rspec-core (~> 3.4.0) 168 | rspec-expectations (~> 3.4.0) 169 | rspec-mocks (~> 3.4.0) 170 | rspec-support (~> 3.4.0) 171 | rspec-support (3.4.1) 172 | slop (3.6.0) 173 | sourcemap (0.1.1) 174 | sprockets (3.6.0) 175 | concurrent-ruby (~> 1.0) 176 | rack (> 1, < 3) 177 | sprockets-rails (2.3.3) 178 | actionpack (>= 3.0) 179 | activesupport (>= 3.0) 180 | sprockets (>= 2.8, < 4.0) 181 | sqlite3 (1.3.11) 182 | therubyracer (0.12.2) 183 | libv8 (~> 3.16.14.0) 184 | ref 185 | thor (0.19.1) 186 | thread_safe (0.3.5) 187 | tilt (2.0.5) 188 | tzinfo (1.2.2) 189 | thread_safe (~> 0.1) 190 | 191 | PLATFORMS 192 | ruby 193 | 194 | DEPENDENCIES 195 | pry 196 | reactive-record! 197 | reactrb! 198 | rspec-rails 199 | sqlite3 200 | 201 | BUNDLED WITH 202 | 1.10.6 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 catprintlabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 YOURNAME 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive Record 2 | 3 | [![Join the chat at https://gitter.im/catprintlabs/reactive-record](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/reactrb/chat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Code Climate](https://codeclimate.com/github/reactrb/reactive-record/badges/gpa.svg)](https://codeclimate.com/github/reactrb/reactive-record) 5 | [![Gem Version](https://badge.fury.io/rb/reactive-record.svg)](https://badge.fury.io/rb/reactive-record) 6 | 7 | 8 | #### reactive-record gives you active-record models on the client integrated with reactrb. 9 | 10 | *"So simple its almost magic" (Amazed developer)* 11 | 12 | #### NOTE: reactive-record >= 0.8.x depends on the reactrb gem. You must [upgrade to reactrb](https://github.com/reactrb/reactrb#upgrading-to-reactrb) 13 | 14 | You do nothing to your current active-record models except move them to the models/public directory (so they are compiled on the client as well as the server.) 15 | 16 | * Fully integrated with [Reactrb](https://github.com/reactrb/reactrb) (which is React with a beautiful ruby dsl.) 17 | * Takes advantage of React prerendering, and afterwards additional data is *lazy loaded* as it is needed by the client. 18 | * Supports full CRUD access using standard Active Record features, including associations, aggregations, and errors. 19 | * Uses model based authorization mechanism for security similar to [Hobo](http://www.hobocentral.net/manual/permissions) or [Pundit](https://github.com/elabs/pundit). 20 | * Models and even methods within models can be selectively implemented "server-side" only. 21 | 22 | There are no docs yet, but you may consider the test cases as a starting point, or have a look at [reactrb todo](https://reactiverb-todo.herokuapp.com/) (live demo [here.](https://reactiverb-todo.herokuapp.com/)) 23 | 24 | For best results simply use the [reactrb-rails-installer](https://github.com/reactrb/reactrb-rails-installer) to install everything you need into a new or existing rails app. 25 | 26 | Head on over to [gitter.im](https://gitter.im/reactrb/chat) to ask any questions you might have! 27 | 28 | Note: We have dropped suppport for the ability to load the same Class from two different files. If you need this functionality load the following code to your config/application.rb file. 29 | 30 | ```ruby 31 | module ::ActiveRecord 32 | module Core 33 | module ClassMethods 34 | def inherited(child_class) 35 | begin 36 | file = Rails.root.join('app','models',"#{child_class.name.underscore}.rb").to_s rescue nil 37 | begin 38 | require file 39 | rescue LoadError 40 | end 41 | # from active record: 42 | child_class.initialize_find_by_cache 43 | rescue 44 | end # if File.exist?(Rails.root.join('app', 'view', 'models.rb')) 45 | super 46 | end 47 | end 48 | end 49 | end 50 | ``` 51 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 8 | load 'rails/tasks/engine.rake' 9 | Bundler::GemHelper.install_tasks 10 | Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f } 11 | require 'rspec/core' 12 | require 'rspec/core/rake_task' 13 | desc "Run all specs in spec directory (excluding plugin specs)" 14 | RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare') 15 | 16 | require 'opal/rspec/rake_task' 17 | require 'bundler' 18 | Bundler.require 19 | 20 | # Add our opal/ directory to the load path 21 | #Opal.append_path File.expand_path('../lib', __FILE__) 22 | 23 | Opal::RSpec::RakeTask.new(:spec_opal) do |s| 24 | s.sprockets.paths.tap { s.sprockets.clear_paths }[0..-2].each { |path| s.sprockets.append_path path} 25 | s.main = 'sprockets_runner' 26 | s.append_path 'spec-opal' 27 | end 28 | 29 | task :default => [:spec, :spec_opal] 30 | -------------------------------------------------------------------------------- /app/controllers/reactive_record/application_controller.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRecord 2 | class ApplicationController < ActionController::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/reactive_record/reactive_record_controller.rb: -------------------------------------------------------------------------------- 1 | require 'reactive_record/server_data_cache' 2 | 3 | module ReactiveRecord 4 | 5 | class ReactiveRecordController < ::ApplicationController 6 | 7 | def fetch 8 | render :json => ReactiveRecord::ServerDataCache[ 9 | (json_params[:models] || []).map(&:with_indifferent_access), 10 | (json_params[:associations] || []).map(&:with_indifferent_access), 11 | json_params[:pending_fetches], 12 | acting_user 13 | ] 14 | rescue Exception => e 15 | render json: {error: e.message, backtrace: e.backtrace}, status: 500 16 | end 17 | 18 | def save 19 | render :json => ReactiveRecord::Base.save_records( 20 | (json_params[:models] || []).map(&:with_indifferent_access), 21 | (json_params[:associations] || []).map(&:with_indifferent_access), 22 | acting_user, 23 | json_params[:validate], 24 | true 25 | ) 26 | rescue Exception => e 27 | render json: {error: e.message, backtrace: e.backtrace}, status: 500 28 | end 29 | 30 | def destroy 31 | render :json => ReactiveRecord::Base.destroy_record( 32 | json_params[:model], 33 | json_params[:id], 34 | json_params[:vector], 35 | acting_user 36 | ) 37 | rescue Exception => e 38 | render json: {error: e.message, backtrace: e.backtrace}, status: 500 39 | end 40 | 41 | private 42 | 43 | def json_params 44 | JSON.parse(params[:json]).symbolize_keys 45 | end 46 | 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ReactiveRecord::Engine.routes.draw do 2 | root :to => "reactive_record#fetch", via: :post 3 | match 'save', to: 'reactive_record#save', via: :post 4 | match 'destroy', to: 'reactive_record#destroy', via: :post 5 | match 'syncromesh-subscribe', to: 'syncromesh#subscribe', via: :get 6 | match 'syncromesh-read/:subscriber', to: 'syncromesh#read', via: :get 7 | end 8 | -------------------------------------------------------------------------------- /lib/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Declare your gem's dependencies in reactive_record.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # jquery-rails is used by the dummy application 9 | gem "jquery-rails" 10 | 11 | # Declare any dependencies that are still in development here instead of in 12 | # your gemspec. These might include edge Rails or gems from your path or 13 | # Git. Remember to move these dependencies to your gemspec before releasing 14 | # your gem to rubygems.org. 15 | 16 | # To use debugger 17 | # gem 'debugger' 18 | -------------------------------------------------------------------------------- /lib/reactive-record.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE == 'opal' 2 | 3 | require "reactrb" 4 | require "reactive_record/active_record/error" 5 | require "reactive_record/server_data_cache" 6 | require "reactive_record/active_record/reactive_record/while_loading" 7 | require "reactive_record/active_record/reactive_record/isomorphic_base" 8 | require "reactive_record/active_record/aggregations" 9 | require "reactive_record/active_record/associations" 10 | require "reactive_record/active_record/reactive_record/base" 11 | require "reactive_record/active_record/reactive_record/collection" 12 | require "reactive_record/reactive_scope" 13 | require "reactive_record/active_record/class_methods" 14 | require "reactive_record/active_record/instance_methods" 15 | require "reactive_record/active_record/base" 16 | require "reactive_record/interval" 17 | 18 | else 19 | 20 | require "opal" 21 | require "reactive_record/version" 22 | require "reactive_record/permissions" 23 | require "reactive_record/engine" 24 | require "reactive_record/server_data_cache" 25 | require "reactive_record/active_record/reactive_record/isomorphic_base" 26 | require "reactive_record/reactive_scope" 27 | require "reactive_record/serializers" 28 | require "reactive_record/pry" 29 | 30 | Opal.append_path File.expand_path('../', __FILE__).untaint 31 | Opal.append_path File.expand_path('../../vendor', __FILE__).untaint 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/reactive_record/active_record/aggregations.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | 3 | class Base 4 | 5 | def self.reflect_on_all_aggregations 6 | base_class.instance_eval { @aggregations ||= [] } 7 | end 8 | 9 | def self.reflect_on_aggregation(attribute) 10 | reflect_on_all_aggregations.detect { |aggregation| aggregation.attribute == attribute } 11 | end 12 | 13 | end 14 | 15 | module Aggregations 16 | 17 | class AggregationReflection 18 | 19 | attr_reader :klass_name 20 | attr_reader :attribute 21 | attr_reader :mapped_attributes 22 | attr_reader :constructor 23 | 24 | def construct(args) 25 | 26 | end 27 | 28 | def initialize(owner_class, macro, name, options = {}) 29 | owner_class.reflect_on_all_aggregations << self 30 | @owner_class = owner_class 31 | @constructor = options[:constructor] || :new 32 | @klass_name = options[:class_name] || name.camelize 33 | @attribute = name 34 | if options[:mapping].respond_to? :collect 35 | @mapped_attributes = options[:mapping].collect &:last 36 | else 37 | ReactiveRecord::Base.log("improper aggregate definition #{@owner_class}, :#{name}, class_name: #{@klass_name} - missing mapping", :error) 38 | @mapped_attributes = [] 39 | end 40 | end 41 | 42 | def klass 43 | @klass ||= Object.const_get(@klass_name) 44 | end 45 | 46 | def serialize(object) 47 | if object.nil? 48 | object # return dummy value if that is what we got 49 | else 50 | @mapped_attributes.collect { |attr| object.send(attr) } 51 | end 52 | end 53 | 54 | def deserialize(array) 55 | if array.nil? 56 | array # return dummy value if that is what we got 57 | elsif @constructor.respond_to?(:call) 58 | @constructor.call(*array) 59 | else 60 | klass.send(@constructor, *array) 61 | end 62 | end 63 | 64 | end 65 | 66 | end 67 | 68 | 69 | end 70 | -------------------------------------------------------------------------------- /lib/reactive_record/active_record/associations.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | 3 | class Base 4 | 5 | def self.reflect_on_all_associations 6 | base_class.instance_eval { @associations ||= superclass.instance_eval { (@associations && @associations.dup) || [] } } 7 | end 8 | 9 | def self.reflect_on_association(attribute) 10 | if found = reflect_on_all_associations.detect { |association| association.attribute == attribute and association.owner_class == self } 11 | found 12 | elsif superclass == Base 13 | nil 14 | else 15 | superclass.reflect_on_association(attribute) 16 | end 17 | end 18 | 19 | end 20 | 21 | module Associations 22 | 23 | class AssociationReflection 24 | 25 | attr_reader :association_foreign_key 26 | attr_reader :attribute 27 | attr_reader :macro 28 | attr_reader :owner_class 29 | 30 | def initialize(owner_class, macro, name, options = {}) 31 | owner_class.reflect_on_all_associations << self 32 | @owner_class = owner_class 33 | @macro = macro 34 | @options = options 35 | @klass_name = options[:class_name] || (collection? && name.camelize.gsub(/s$/,"")) || name.camelize 36 | if @klass_name < ActiveRecord::Base 37 | @klass = @klass_name 38 | @klass_name = @klass_name.name 39 | end rescue nil 40 | @association_foreign_key = options[:foreign_key] || (macro == :belongs_to && "#{name}_id") || "#{@owner_class.name.underscore}_id" 41 | @attribute = name 42 | end 43 | 44 | def inverse_of 45 | unless @options[:through] or @inverse_of 46 | inverse_association = klass.reflect_on_all_associations.detect do | association | 47 | association.association_foreign_key == @association_foreign_key and association.klass == @owner_class and association.attribute != attribute and klass == association.owner_class 48 | end 49 | raise "Association #{@owner_class}.#{attribute} (foreign_key: #{@association_foreign_key}) has no inverse in #{@klass_name}" unless inverse_association 50 | @inverse_of = inverse_association.attribute 51 | end 52 | @inverse_of 53 | end 54 | 55 | def klass 56 | @klass ||= Object.const_get(@klass_name) 57 | end 58 | 59 | def collection? 60 | [:has_many].include? @macro 61 | end 62 | 63 | end 64 | 65 | end 66 | 67 | 68 | end 69 | -------------------------------------------------------------------------------- /lib/reactive_record/active_record/base.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | class Base 3 | 4 | extend ClassMethods 5 | 6 | include InstanceMethods 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/reactive_record/active_record/class_methods.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | 3 | module ClassMethods 4 | 5 | def base_class 6 | 7 | unless self < Base 8 | raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" 9 | end 10 | 11 | if superclass == Base || superclass.abstract_class? 12 | self 13 | else 14 | superclass.base_class 15 | end 16 | 17 | end 18 | 19 | def abstract_class? 20 | defined?(@abstract_class) && @abstract_class == true 21 | end 22 | 23 | def primary_key 24 | base_class.instance_eval { @primary_key_value || :id } 25 | end 26 | 27 | def primary_key=(val) 28 | base_class.instance_eval { @primary_key_value = val } 29 | end 30 | 31 | def inheritance_column 32 | base_class.instance_eval {@inheritance_column_value || "type"} 33 | end 34 | 35 | def inheritance_column=(name) 36 | base_class.instance_eval {@inheritance_column_value = name} 37 | end 38 | 39 | def model_name 40 | # in reality should return ActiveModel::Name object, blah blah 41 | name 42 | end 43 | 44 | def find(id) 45 | base_class.instance_eval {ReactiveRecord::Base.find(self, primary_key, id)} 46 | end 47 | 48 | def find_by(opts = {}) 49 | base_class.instance_eval {ReactiveRecord::Base.find(self, opts.first.first, opts.first.last)} 50 | end 51 | 52 | def enum(*args) 53 | # when we implement schema validation we should also implement value checking 54 | end 55 | 56 | def method_missing(name, *args, &block) 57 | if args.count == 1 && name =~ /^find_by_/ && !block 58 | find_by(name.gsub(/^find_by_/, "") => args[0]) 59 | else 60 | raise "#{self.name}.#{name}(#{args}) (called class method missing)" 61 | end 62 | end 63 | 64 | def abstract_class=(val) 65 | @abstract_class = val 66 | end 67 | 68 | def scope(name, body) 69 | singleton_class.send(:define_method, name) do | *args | 70 | args = (args.count == 0) ? name : [name, *args] 71 | ReactiveRecord::Base.class_scopes(self)[args] ||= ReactiveRecord::Collection.new(self, nil, nil, self, args) 72 | end 73 | singleton_class.send(:define_method, "#{name}=") do |collection| 74 | ReactiveRecord::Base.class_scopes(self)[name] = collection 75 | end 76 | end 77 | 78 | def all 79 | ReactiveRecord::Base.class_scopes(self)[:all] ||= ReactiveRecord::Collection.new(self, nil, nil, self, "all") 80 | end 81 | 82 | def all=(collection) 83 | ReactiveRecord::Base.class_scopes(self)[:all] = collection 84 | end 85 | 86 | # def server_methods(*methods) 87 | # methods.each do |method| 88 | # define_method(method) do |*args| 89 | # if args.count == 0 90 | # @backing_record.reactive_get!(method, :initialize) 91 | # else 92 | # @backing_record.reactive_get!([[method]+args], :initialize) 93 | # end 94 | # end 95 | # define_method("#{method}!") do |*args| 96 | # if args.count == 0 97 | # @backing_record.reactive_get!(method, :force) 98 | # else 99 | # @backing_record.reactive_get!([[method]+args], :force) 100 | # end 101 | # end 102 | # end 103 | # end 104 | # 105 | # alias_method :server_method, :server_methods 106 | 107 | [:belongs_to, :has_many, :has_one].each do |macro| 108 | define_method(macro) do |*args| # is this a bug in opal? saying name, scope=nil, opts={} does not work! 109 | name = args.first 110 | opts = (args.count > 1 and args.last.is_a? Hash) ? args.last : {} 111 | Associations::AssociationReflection.new(self, macro, name, opts) 112 | end 113 | end 114 | 115 | def composed_of(name, opts = {}) 116 | Aggregations::AggregationReflection.new(base_class, :composed_of, name, opts) 117 | end 118 | 119 | def column_names 120 | [] # it would be great to figure out how to get this information on the client! For now we just return an empty array 121 | end 122 | 123 | [ 124 | "table_name=", "before_validation", "with_options", "validates_presence_of", "validates_format_of", 125 | "accepts_nested_attributes_for", "before_create", "after_create", "before_save", "after_save", "before_destroy", "where", "validate", 126 | "attr_protected", "validates_numericality_of", "default_scope", "has_attached_file", "attr_accessible", 127 | "serialize" 128 | ].each do |method| 129 | define_method(method.to_s) { |*args, &block| } 130 | end 131 | 132 | def _react_param_conversion(param, opt = nil) 133 | # defines how react will convert incoming json to this ActiveRecord model 134 | #TIMING times = {start: Time.now.to_f, json_start: 0, json_end: 0, db_load_start: 0, db_load_end: 0} 135 | #TIMING times[:json_start] = Time.now.to_f 136 | param = Native(param) 137 | param = JSON.from_object(param.to_n) if param.is_a? Native::Object 138 | #TIMING times[:json_end] = Time.now.to_f 139 | result = if param.is_a? self 140 | param 141 | elsif param.is_a? Hash 142 | if opt == :validate_only 143 | klass = ReactiveRecord::Base.infer_type_from_hash(self, param) 144 | klass == self or klass < self 145 | else 146 | if param[primary_key] 147 | target = find(param[primary_key]) 148 | else 149 | target = new 150 | end 151 | #TIMING times[:db_load_start] = Time.now.to_f 152 | ReactiveRecord::Base.load_from_json(Hash[param.collect { |key, value| [key, [value]] }], target) 153 | #TIMING times[:db_load_end] = Time.now.to_f 154 | target 155 | end 156 | else 157 | nil 158 | end 159 | #TIMING times[:end] = Time.now.to_f 160 | #TIMING puts "times - total: #{'%.04f' % (times[:end]-times[:start])}, native conversion: #{'%.04f' % (times[:json_end]-times[:json_start])}, loading: #{'%.04f' % (times[:db_load_end]-times[:db_load_start])}" 161 | result 162 | end 163 | 164 | end 165 | 166 | end 167 | -------------------------------------------------------------------------------- /lib/reactive_record/active_record/error.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | 3 | class Error 4 | 5 | attr_reader :messages 6 | 7 | def initialize(msgs = {}) 8 | @messages = msgs || {} 9 | @messages.each { |attribute, messages| @messages[attribute] = messages.uniq } 10 | end 11 | 12 | def [](attribute) 13 | messages[attribute] 14 | end 15 | 16 | def delete(attribute) 17 | messages.delete(attribute) 18 | end 19 | 20 | def empty? 21 | messages.empty? 22 | end 23 | 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /lib/reactive_record/active_record/instance_methods.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | 3 | module InstanceMethods 4 | 5 | attr_reader :backing_record 6 | 7 | def attributes 8 | @backing_record.attributes 9 | end 10 | 11 | def initialize(hash = {}) 12 | 13 | if hash.is_a? ReactiveRecord::Base 14 | @backing_record = hash 15 | else 16 | # standard active_record new -> creates a new instance, primary key is ignored if present 17 | # we have to build the backing record first then initialize it so associations work correctly 18 | @backing_record = ReactiveRecord::Base.new(self.class, {}, self) 19 | @backing_record.instance_eval do 20 | self.class.load_data do 21 | hash.each do |attribute, value| 22 | unless attribute == primary_key 23 | reactive_set!(attribute, value) 24 | changed_attributes << attribute 25 | end 26 | end 27 | #changed_attributes << primary_key # insures that changed attributes has at least one element 28 | end 29 | end 30 | end 31 | end 32 | 33 | def primary_key 34 | self.class.primary_key 35 | end 36 | 37 | def id 38 | @backing_record.reactive_get!(primary_key) 39 | end 40 | 41 | def id=(value) 42 | @backing_record.id = value 43 | end 44 | 45 | def model_name 46 | # in reality should return ActiveModel::Name object, blah blah 47 | self.class.model_name 48 | end 49 | 50 | def revert 51 | @backing_record.revert 52 | end 53 | 54 | def changed? 55 | @backing_record.changed? 56 | end 57 | 58 | def dup 59 | self.class.new(self.attributes) 60 | end 61 | 62 | def ==(ar_instance) 63 | @backing_record == ar_instance.instance_eval { @backing_record } 64 | end 65 | 66 | def method_missing(name, *args, &block) 67 | if name =~ /\!$/ 68 | name = name.gsub(/\!$/,"") 69 | force_update = true 70 | end 71 | if name =~ /_changed\?$/ 72 | @backing_record.changed?(name.gsub(/_changed\?$/,"")) 73 | elsif args.count == 1 && name =~ /=$/ && !block 74 | attribute_name = name.gsub(/=$/,"") 75 | @backing_record.reactive_set!(attribute_name, args[0]) 76 | elsif args.count == 0 && !block 77 | @backing_record.reactive_get!(name, force_update) 78 | elsif !block 79 | @backing_record.reactive_get!([[name]+args], force_update) 80 | else 81 | super 82 | end 83 | end 84 | 85 | def save(opts = {}, &block) 86 | @backing_record.save(opts.has_key?(:validate) ? opts[:validate] : true, opts[:force], &block) 87 | end 88 | 89 | def saving? 90 | @backing_record.saving? 91 | end 92 | 93 | def destroy(&block) 94 | @backing_record.destroy &block 95 | end 96 | 97 | def destroyed? 98 | @backing_record.destroyed 99 | end 100 | 101 | def new? 102 | @backing_record.new? 103 | end 104 | 105 | def errors 106 | React::State.get_state(@backing_record, @backing_record) 107 | @backing_record.errors 108 | end 109 | 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /lib/reactive_record/active_record/reactive_record/collection.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRecord 2 | 3 | class Collection 4 | 5 | def initialize(target_klass, owner = nil, association = nil, *vector) 6 | @owner = owner # can be nil if this is an outer most scope 7 | @association = association 8 | @target_klass = target_klass 9 | if owner and !owner.id and vector.length <= 1 10 | @collection = [] 11 | elsif vector.length > 0 12 | @vector = vector 13 | elsif owner 14 | @vector = owner.backing_record.vector + [association.attribute] 15 | else 16 | @vector = [target_klass] 17 | end 18 | @scopes = {} 19 | end 20 | 21 | def dup_for_sync 22 | self.dup.instance_eval do 23 | @collection = @collection.dup if @collection 24 | @scopes = @scopes.dup 25 | self 26 | end 27 | end 28 | 29 | def all 30 | observed 31 | @dummy_collection.notify if @dummy_collection 32 | unless @collection 33 | @collection = [] 34 | if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"]) 35 | ids.each do |id| 36 | @collection << @target_klass.find_by(@target_klass.primary_key => id) 37 | end 38 | else 39 | @dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all") 40 | @dummy_record = self[0] 41 | end 42 | end 43 | @collection 44 | end 45 | 46 | def [](index) 47 | observed 48 | if (@collection || all).length <= index and @dummy_collection 49 | (@collection.length..index).each do |i| 50 | new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}") 51 | new_dummy_record.backing_record.attributes[@association.inverse_of] = @owner if @association and @association.inverse_of 52 | @collection << new_dummy_record 53 | end 54 | end 55 | @collection[index] 56 | end 57 | 58 | def ==(other_collection) 59 | observed 60 | return !@collection unless other_collection.is_a? Collection 61 | other_collection.observed 62 | my_collection = (@collection || []).select { |target| target != @dummy_record } 63 | other_collection = (other_collection ? (other_collection.collection || []) : []).select { |target| target != other_collection.dummy_record } 64 | my_collection == other_collection 65 | end 66 | 67 | def apply_scope(scope, *args) 68 | # The value returned is another ReactiveRecordCollection with the scope added to the vector 69 | # no additional action is taken 70 | scope = [scope, *args] if args.count > 0 71 | @scopes[scope] ||= Collection.new(@target_klass, @owner, @association, *@vector, [scope]) 72 | end 73 | 74 | def count 75 | observed 76 | if @collection 77 | @collection.count 78 | elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"]) 79 | @count 80 | else 81 | ReactiveRecord::Base.load_from_db(nil, *@vector, "*count") 82 | @count = 1 83 | end 84 | end 85 | 86 | alias_method :length, :count 87 | 88 | def proxy_association 89 | @association || self # returning self allows this to work with things like Model.all 90 | end 91 | 92 | def klass 93 | @target_klass 94 | end 95 | 96 | def <<(item) 97 | return delete(item) if item.destroyed? # pushing a destroyed item is the same as removing it 98 | backing_record = item.backing_record 99 | all << item unless all.include? item # does this use == if so we are okay... 100 | if backing_record and @owner and @association and inverse_of = @association.inverse_of and item.attributes[inverse_of] != @owner 101 | current_association = item.attributes[inverse_of] 102 | backing_record.virgin = false unless backing_record.data_loading? 103 | backing_record.update_attribute(inverse_of, @owner) 104 | current_association.attributes[@association.attribute].delete(item) if current_association and current_association.attributes[@association.attribute] 105 | @owner.backing_record.update_attribute(@association.attribute) # forces a check if association contents have changed from synced values 106 | end 107 | if item.id and @dummy_record 108 | @dummy_record.id = item.id 109 | @collection.delete(@dummy_record) 110 | @dummy_record = @collection.detect { |r| r.backing_record.vector.last =~ /^\*[0-9]+$/ } 111 | @dummy_collection = nil 112 | end 113 | notify_of_change self 114 | end 115 | 116 | [:first, :last].each do |method| 117 | define_method method do |*args| 118 | if args.count == 0 119 | all.send(method) 120 | else 121 | apply_scope(method, *args) 122 | end 123 | end 124 | end 125 | 126 | def replace(new_array) 127 | 128 | # not tested if you do all[n] where n > 0... this will create additional dummy items, that this will not sync up. 129 | # probably just moving things around so the @dummy_collection and @dummy_record are updated AFTER the new items are pushed 130 | # should work. 131 | 132 | if @dummy_collection 133 | @dummy_collection.notify 134 | array = new_array.is_a?(Collection) ? new_array.collection : new_array 135 | @collection.each_with_index do |r, i| 136 | r.id = new_array[i].id if array[i] and array[i].id and r.backing_record.vector.last =~ /^\*[0-9]+$/ 137 | end 138 | end 139 | 140 | @collection.dup.each { |item| delete(item) } if @collection # this line is a big nop I think 141 | @collection = [] 142 | if new_array.is_a? Collection 143 | @dummy_collection = new_array.dummy_collection 144 | @dummy_record = new_array.dummy_record 145 | new_array.collection.each { |item| self << item } if new_array.collection 146 | else 147 | @dummy_collection = @dummy_record = nil 148 | new_array.each { |item| self << item } 149 | end 150 | notify_of_change new_array 151 | end 152 | 153 | def delete(item) 154 | notify_of_change(if @owner and @association and inverse_of = @association.inverse_of 155 | if backing_record = item.backing_record and backing_record.attributes[inverse_of] == @owner 156 | # the if prevents double update if delete is being called from << (see << above) 157 | backing_record.update_attribute(inverse_of, nil) 158 | end 159 | all.delete(item).tap { @owner.backing_record.update_attribute(@association.attribute) } # forces a check if association contents have changed from synced values 160 | else 161 | all.delete(item) 162 | end) 163 | end 164 | 165 | def loading? 166 | all # need to force initialization at this point 167 | @dummy_collection.loading? 168 | end 169 | 170 | def empty? # should be handled by method missing below, but opal-rspec does not deal well with method missing, so to test... 171 | all.empty? 172 | end 173 | 174 | def method_missing(method, *args, &block) 175 | if [].respond_to? method 176 | all.send(method, *args, &block) 177 | elsif @target_klass.respond_to?(method) or (args.count == 1 && method =~ /^find_by_/) 178 | apply_scope(method, *args) 179 | else 180 | super 181 | end 182 | end 183 | 184 | protected 185 | 186 | def dummy_record 187 | @dummy_record 188 | end 189 | 190 | def collection 191 | @collection 192 | end 193 | 194 | def dummy_collection 195 | @dummy_collection 196 | end 197 | 198 | def notify_of_change(value = nil) 199 | React::State.set_state(self, "collection", collection) unless ReactiveRecord::Base.data_loading? 200 | value 201 | end 202 | 203 | def observed 204 | React::State.get_state(self, "collection") unless ReactiveRecord::Base.data_loading? 205 | end 206 | 207 | end 208 | 209 | end 210 | -------------------------------------------------------------------------------- /lib/reactive_record/active_record/reactive_record/while_loading.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRecord 2 | 3 | # will repeatedly execute the block until it is loaded 4 | # immediately returns a promise that will resolve once the block is loaded 5 | 6 | def self.load(&block) 7 | promise = Promise.new 8 | @load_stack ||= [] 9 | @load_stack << @loads_pending 10 | @loads_pending = nil 11 | result = block.call 12 | if @loads_pending 13 | @blocks_to_load ||= [] 14 | @blocks_to_load << [Base.last_fetch_at, promise, block] 15 | else 16 | promise.resolve result 17 | end 18 | @loads_pending = @load_stack.pop 19 | promise 20 | rescue Exception => e 21 | React::IsomorphicHelpers.log "ReactiveRecord.load exception raised during initial load: #{e}", :error 22 | end 23 | 24 | def self.loads_pending! 25 | @loads_pending = true 26 | end 27 | 28 | def self.check_loads_pending 29 | if @loads_pending 30 | if Base.pending_fetches.count > 0 31 | true 32 | else # this happens when for example loading foo.x results in somebody looking at foo.y while foo.y is still being loaded 33 | ReactiveRecord::WhileLoading.loaded_at Base.last_fetch_at 34 | ReactiveRecord::WhileLoading.quiet! 35 | false 36 | end 37 | end 38 | end 39 | 40 | def self.run_blocks_to_load(fetch_id, failure = nil) 41 | if @blocks_to_load 42 | blocks_to_load_now = @blocks_to_load.select { |data| data.first == fetch_id } 43 | @blocks_to_load = @blocks_to_load.reject { |data| data.first == fetch_id } 44 | @load_stack ||= [] 45 | blocks_to_load_now.each do |data| 46 | id, promise, block = data 47 | @load_stack << @loads_pending 48 | @loads_pending = nil 49 | result = block.call(failure) 50 | if check_loads_pending and !failure 51 | @blocks_to_load << [Base.last_fetch_at, promise, block] 52 | else 53 | promise.resolve result 54 | end 55 | @loads_pending = @load_stack.pop 56 | end 57 | end 58 | rescue Exception => e 59 | React::IsomorphicHelpers.log "ReactiveRecord.load exception raised during retry: #{e}", :error 60 | end 61 | 62 | 63 | # Adds while_loading feature to React 64 | # to use attach a .while_loading handler to any element for example 65 | # div { "displayed if everything is loaded" }.while_loading { "displayed while I'm loading" } 66 | # the contents of the div will be switched (using jQuery.show/hide) depending on the state of contents of the first block 67 | 68 | # To notify React that something is loading use React::WhileLoading.loading! 69 | # once everything is loaded then do React::WhileLoading.loaded_at message (typically a time stamp just for debug purposes) 70 | 71 | class WhileLoading 72 | 73 | include React::IsomorphicHelpers 74 | 75 | before_first_mount do 76 | @css_to_preload = "" 77 | @while_loading_counter = 0 78 | end 79 | 80 | def get_next_while_loading_counter 81 | @while_loading_counter += 1 82 | end 83 | 84 | def preload_css(css) 85 | @css_to_preload << css << "\n" 86 | end 87 | 88 | prerender_footer do 89 | "".tap { @css_to_preload = ""} 90 | end 91 | 92 | if RUBY_ENGINE == 'opal' 93 | 94 | # I DONT THINK WE USE opal-jquery in this module anymore - require 'opal-jquery' if opal_client? 95 | 96 | include React::Component 97 | 98 | param :loading 99 | param :loaded_children 100 | param :loading_children 101 | param :element_type 102 | param :element_props 103 | param :display, default: "" 104 | 105 | class << self 106 | 107 | def loading? 108 | @is_loading 109 | end 110 | 111 | def loading! 112 | React::RenderingContext.waiting_on_resources = true 113 | React::State.get_state(self, :loaded_at) 114 | React::State.set_state(self, :quiet, false) 115 | @is_loading = true 116 | end 117 | 118 | def loaded_at(loaded_at) 119 | React::State.set_state(self, :loaded_at, loaded_at) 120 | @is_loading = false 121 | end 122 | 123 | def quiet? 124 | React::State.get_state(self, :quiet) 125 | end 126 | 127 | def page_loaded? 128 | React::State.get_state(self, :page_loaded) 129 | end 130 | 131 | def quiet! 132 | React::State.set_state(self, :quiet, true) 133 | after(1) { React::State.set_state(self, :page_loaded, true) } unless on_opal_server? or @page_loaded 134 | @page_loaded = true 135 | end 136 | 137 | def add_style_sheet 138 | @style_sheet ||= %x{ 139 | $('').appendTo("head") 143 | } 144 | end 145 | 146 | end 147 | 148 | before_mount do 149 | @uniq_id = WhileLoading.get_next_while_loading_counter 150 | WhileLoading.preload_css( 151 | ".reactive_record_while_loading_container_#{@uniq_id} > :nth-child(1n+#{loaded_children.count+1}) {\n"+ 152 | " display: none;\n"+ 153 | "}\n" 154 | ) 155 | end 156 | 157 | after_mount do 158 | @waiting_on_resources = loading 159 | WhileLoading.add_style_sheet 160 | %x{ 161 | var node = #{dom_node}; 162 | $(node).children(':nth-child(-1n+'+#{loaded_children.count}+')').addClass('reactive_record_show_when_loaded'); 163 | $(node).children(':nth-child(1n+'+#{loaded_children.count+1}+')').addClass('reactive_record_show_while_loading'); 164 | } 165 | end 166 | 167 | after_update do 168 | @waiting_on_resources = loading 169 | end 170 | 171 | def render 172 | props = element_props.dup 173 | classes = [props[:class], props[:className], "reactive_record_while_loading_container_#{@uniq_id}"].compact.join(" ") 174 | props.merge!({ 175 | "data-reactive_record_while_loading_container_id" => @uniq_id, 176 | "data-reactive_record_enclosing_while_loading_container_id" => @uniq_id, 177 | class: classes 178 | }) 179 | React.create_element(element_type, props) { loaded_children + loading_children } 180 | end 181 | 182 | end 183 | 184 | end 185 | 186 | end 187 | 188 | module React 189 | 190 | class Element 191 | 192 | def while_loading(display = "", &loading_display_block) 193 | 194 | loaded_children = [] 195 | loaded_children = block.call.dup if block 196 | 197 | loading_children = [display] 198 | loading_children = RenderingContext.build do |buffer| 199 | result = loading_display_block.call 200 | buffer << result.to_s if result.is_a? String 201 | buffer.dup 202 | end if loading_display_block 203 | RenderingContext.replace( 204 | self, 205 | React.create_element( 206 | ReactiveRecord::WhileLoading, 207 | loading: waiting_on_resources, 208 | loading_children: loading_children, 209 | loaded_children: loaded_children, 210 | element_type: type, 211 | element_props: properties) 212 | ) 213 | end 214 | 215 | def hide_while_loading 216 | while_loading 217 | end 218 | 219 | end 220 | 221 | module Component 222 | 223 | alias_method :original_component_did_mount, :component_did_mount 224 | 225 | def component_did_mount(*args) 226 | original_component_did_mount(*args) 227 | reactive_record_link_to_enclosing_while_loading_container 228 | reactive_record_link_set_while_loading_container_class 229 | end 230 | 231 | alias_method :original_component_did_update, :component_did_update 232 | 233 | def component_did_update(*args) 234 | original_component_did_update(*args) 235 | reactive_record_link_set_while_loading_container_class 236 | end 237 | 238 | def reactive_record_link_to_enclosing_while_loading_container 239 | # Call after any component mounts - attaches the containers loading id to this component 240 | # Fyi, the while_loading container is responsible for setting its own link to itself 241 | 242 | %x{ 243 | var node = #{dom_node}; 244 | if (!$(node).is('[data-reactive_record_enclosing_while_loading_container_id]')) { 245 | var while_loading_container = $(node).closest('[data-reactive_record_while_loading_container_id]') 246 | if (while_loading_container.length > 0) { 247 | var container_id = $(while_loading_container).attr('data-reactive_record_while_loading_container_id') 248 | $(node).attr('data-reactive_record_enclosing_while_loading_container_id', container_id) 249 | } 250 | } 251 | } 252 | 253 | end 254 | 255 | def reactive_record_link_set_while_loading_container_class 256 | 257 | %x{ 258 | 259 | var node = #{dom_node}; 260 | var while_loading_container_id = $(node).attr('data-reactive_record_enclosing_while_loading_container_id'); 261 | if (while_loading_container_id) { 262 | var while_loading_container = $('[data-reactive_record_while_loading_container_id='+while_loading_container_id+']'); 263 | var loading = (#{waiting_on_resources} == true); 264 | if (loading) { 265 | $(node).addClass('reactive_record_is_loading'); 266 | $(node).removeClass('reactive_record_is_loaded'); 267 | $(while_loading_container).addClass('reactive_record_is_loading'); 268 | $(while_loading_container).removeClass('reactive_record_is_loaded'); 269 | 270 | } else if (!$(node).hasClass('reactive_record_is_loaded')) { 271 | 272 | if (!$(node).attr('data-reactive_record_while_loading_container_id')) { 273 | $(node).removeClass('reactive_record_is_loading'); 274 | $(node).addClass('reactive_record_is_loaded'); 275 | } 276 | if (!$(while_loading_container).hasClass('reactive_record_is_loaded')) { 277 | var loading_children = $(while_loading_container). 278 | find('[data-reactive_record_enclosing_while_loading_container_id='+while_loading_container_id+'].reactive_record_is_loading') 279 | if (loading_children.length == 0) { 280 | $(while_loading_container).removeClass('reactive_record_is_loading') 281 | $(while_loading_container).addClass('reactive_record_is_loaded') 282 | } 283 | } 284 | 285 | } 286 | 287 | } 288 | } 289 | 290 | end 291 | 292 | end if RUBY_ENGINE == 'opal' 293 | 294 | end 295 | -------------------------------------------------------------------------------- /lib/reactive_record/engine.rb: -------------------------------------------------------------------------------- 1 | #require 'rails' 2 | 3 | module ReactiveRecord 4 | class Engine < ::Rails::Engine 5 | isolate_namespace ReactiveRecord 6 | config.generators do |g| 7 | g.test_framework :rspec, :fixture => false 8 | g.fixture_replacement :factory_girl, :dir => 'spec/factories' 9 | g.assets false 10 | g.helper false 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/reactive_record/interval.rb: -------------------------------------------------------------------------------- 1 | module Browser 2 | 3 | # Allows you to create an interval that executes the function every given 4 | # seconds. 5 | # 6 | # @see https://developer.mozilla.org/en-US/docs/Web/API/Window.setInterval 7 | class Interval 8 | # @!attribute [r] every 9 | # @return [Float] the seconds every which the block is called 10 | attr_reader :every 11 | 12 | # Create and start an interval. 13 | # 14 | # @param window [Window] the window to start the interval on 15 | # @param time [Float] seconds every which to call the block 16 | def initialize(window, time, &block) 17 | @window = Native.convert(window) 18 | @every = time 19 | @block = block 20 | 21 | @aborted = false 22 | end 23 | 24 | # Check if the interval has been stopped. 25 | def stopped? 26 | @id.nil? 27 | end 28 | 29 | # Check if the interval has been aborted. 30 | def aborted? 31 | @aborted 32 | end 33 | 34 | # Abort the interval, it won't be possible to start it again. 35 | def abort 36 | `#@window.clearInterval(#@id)` 37 | 38 | @aborted = true 39 | @id = nil 40 | end 41 | 42 | # Stop the interval, it will be possible to start it again. 43 | def stop 44 | return if stopped? 45 | 46 | `#@window.clearInterval(#@id)` 47 | 48 | @stopped = true 49 | @id = nil 50 | end 51 | 52 | # Start the interval if it has been stopped. 53 | def start 54 | raise "the interval has been aborted" if aborted? 55 | return unless stopped? 56 | 57 | @id = `#@window.setInterval(#@block, #@every * 1000)` 58 | end 59 | 60 | # Call the [Interval] block. 61 | def call 62 | @block.call 63 | end 64 | end 65 | 66 | class Window 67 | # Execute the block every given seconds. 68 | # 69 | # @param time [Float] the seconds between every call 70 | # 71 | # @return [Interval] the object representing the interval 72 | def every(time, &block) 73 | Interval.new(@native, time, &block).tap(&:start) 74 | end 75 | 76 | # Execute the block every given seconds, you have to call [#start] on it 77 | # yourself. 78 | # 79 | # @param time [Float] the seconds between every call 80 | # 81 | # @return [Interval] the object representing the interval 82 | def every!(time, &block) 83 | Interval.new(@native, time, &block) 84 | end 85 | end 86 | 87 | end 88 | 89 | module Kernel 90 | # (see Browser::Window#every) 91 | def every(time, &block) 92 | $window.every(time, &block) 93 | end 94 | 95 | # (see Browser::Window#every!) 96 | def every!(time, &block) 97 | $window.every!(time, &block) 98 | end 99 | end 100 | 101 | class Proc 102 | # (see Browser::Window#every) 103 | def every(time) 104 | $window.every(time, &self) 105 | end 106 | 107 | # (see Browser::Window#every!) 108 | def every!(time) 109 | $window.every!(time, &self) 110 | end 111 | end 112 | 113 | module Browser 114 | 115 | # Allows you to delay the call to a function which gets called after the 116 | # given time. 117 | # 118 | # @see https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout 119 | class Delay 120 | # @!attribute [r] after 121 | # @return [Float] the seconds after which the block is called 122 | attr_reader :after 123 | 124 | # Create and start a timeout. 125 | # 126 | # @param window [Window] the window to start the timeout on 127 | # @param time [Float] seconds after which the block is called 128 | def initialize(window, time, &block) 129 | @window = Native.convert(window) 130 | @after = time 131 | @block = block 132 | end 133 | 134 | # Abort the timeout. 135 | def abort 136 | `#@window.clearTimeout(#@id)` 137 | end 138 | 139 | # Start the delay. 140 | def start 141 | @id = `#@window.setTimeout(#{@block.to_n}, #@after * 1000)` 142 | end 143 | end 144 | 145 | class Window 146 | # Execute a block after the given seconds. 147 | # 148 | # @param time [Float] the seconds after it gets called 149 | # 150 | # @return [Delay] the object representing the timeout 151 | def after(time, &block) 152 | Delay.new(@native, time, &block).tap(&:start) 153 | end 154 | 155 | # Execute a block after the given seconds, you have to call [#start] on it 156 | # yourself. 157 | # 158 | # @param time [Float] the seconds after it gets called 159 | # 160 | # @return [Delay] the object representing the timeout 161 | def after!(time, &block) 162 | Delay.new(@native, time, &block) 163 | end 164 | end 165 | 166 | end 167 | 168 | module Kernel 169 | # (see Browser::Window#after) 170 | def after(time, &block) 171 | `setTimeout(#{block.to_n}, time * 1000)` 172 | end 173 | 174 | # (see Browser::Window#after!) 175 | def after!(time, &block) 176 | `setTimeout(#{block.to_n}, time * 1000)` 177 | end 178 | end 179 | 180 | class Proc 181 | # (see Browser::Window#after) 182 | def after(time) 183 | $window.after(time, &self) 184 | end 185 | 186 | # (see Browser::Window#after!) 187 | def after!(time) 188 | $window.after!(time, &self) 189 | end 190 | end -------------------------------------------------------------------------------- /lib/reactive_record/permissions.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRecord 2 | class AccessViolation < StandardError 3 | def message 4 | "ReactiveRecord::AccessViolation: #{super}" 5 | end 6 | end 7 | end 8 | 9 | class ActiveRecord::Base 10 | 11 | attr_accessor :acting_user 12 | 13 | def create_permitted? 14 | true 15 | end 16 | 17 | def update_permitted? 18 | true 19 | end 20 | 21 | def destroy_permitted? 22 | true 23 | end 24 | 25 | def view_permitted?(attribute) 26 | true 27 | end 28 | 29 | def only_changed?(*attributes) 30 | (self.attributes.keys + self.class.reactive_record_association_keys).each do |key| 31 | return false if self.send("#{key}_changed?") and !attributes.include? key 32 | end 33 | true 34 | end 35 | 36 | def none_changed?(*attributes) 37 | attributes.each do |key| 38 | return false if self.send("#{key}_changed?") 39 | end 40 | true 41 | end 42 | 43 | def any_changed?(*attributes) 44 | attributes.each do |key| 45 | return true if self.send("#{key}_changed?") 46 | end 47 | false 48 | end 49 | 50 | def all_changed?(*attributes) 51 | attributes.each do |key| 52 | return false unless self.send("#{key}_changed?") 53 | end 54 | true 55 | end 56 | 57 | class << self 58 | 59 | attr_reader :reactive_record_association_keys 60 | 61 | [:has_many, :belongs_to, :composed_of].each do |macro| 62 | define_method "#{macro}_with_reactive_record_add_changed_method".to_sym do |attr_name, *args, &block| 63 | define_method "#{attr_name}_changed?".to_sym do 64 | instance_variable_get "@reactive_record_#{attr_name}_changed".to_sym 65 | end 66 | (@reactive_record_association_keys ||= []) << attr_name 67 | send "#{macro}_without_reactive_record_add_changed_method".to_sym, attr_name, *args, &block 68 | end 69 | alias_method_chain macro, :reactive_record_add_changed_method 70 | end 71 | 72 | def belongs_to_with_reactive_record_add_is_method(attr_name, scope = nil, options = {}) 73 | define_method "#{attr_name}_is?".to_sym do |model| 74 | send(options[:foreign_key] || "#{attr_name}_id") == model.id 75 | end 76 | belongs_to_without_reactive_record_add_is_method(attr_name, scope, options) 77 | end 78 | 79 | alias_method_chain :belongs_to, :reactive_record_add_is_method 80 | 81 | end 82 | 83 | 84 | def check_permission_with_acting_user(user, permission, *args) 85 | old = acting_user 86 | self.acting_user = user 87 | if self.send(permission, *args) 88 | self.acting_user = old 89 | self 90 | else 91 | raise ReactiveRecord::AccessViolation, "for #{permission}(#{args})" 92 | end 93 | end 94 | 95 | end 96 | 97 | class ActionController::Base 98 | 99 | def acting_user 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /lib/reactive_record/pry.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRecord 2 | 3 | module Pry 4 | 5 | def self.rescued(e) 6 | if defined?(PryRescue) && e.instance_variable_defined?(:@rescue_bindings) && !e.is_a?(ReactiveRecord::AccessViolation) 7 | ::Pry::rescued(e) 8 | end 9 | end 10 | 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lib/reactive_record/reactive_scope.rb: -------------------------------------------------------------------------------- 1 | class ActiveRecord::Base 2 | 3 | def self.to_sync(scope_name, opts={}, &block) 4 | watch_list = if opts[:watch] 5 | [*opts.delete[:watch]] 6 | else 7 | [self] 8 | end 9 | if RUBY_ENGINE=='opal' 10 | watch_list.each do |klass_to_watch| 11 | ReactiveRecord::Base.sync_blocks[klass_to_watch][self][scope_name] << block 12 | end 13 | else 14 | # this is where we put server side watchers in place to sync all clients! 15 | end 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/reactive_record/serializers.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Base.send(:define_method, :react_serializer) do 2 | serializable_hash.merge(ReactiveRecord::Base.get_type_hash(self)) 3 | end 4 | 5 | ActiveRecord::Relation.send(:define_method, :react_serializer) do 6 | all.to_a.react_serializer 7 | end 8 | -------------------------------------------------------------------------------- /lib/reactive_record/version.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRecord 2 | VERSION = "0.9.0" 3 | end 4 | -------------------------------------------------------------------------------- /reactive-record.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "reactive_record/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | 9 | s.name = "reactive-record" 10 | s.version = ReactiveRecord::VERSION 11 | s.authors = "Mitch VanDuyn" 12 | s.email = ["mitch@catprint.com"] 13 | s.summary = %q{Access active-record models inside Reactrb components.} 14 | s.description = %q{Access active-record models inside Reactrb components. Model data is calculated during pre-rerendering, and then dynamically loaded as components update.} 15 | 16 | s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"] 17 | 18 | s.test_files = Dir["spec-server/**/*"] 19 | 20 | s.add_dependency "rails", ">= 3.2.13" 21 | 22 | s.add_development_dependency "sqlite3" 23 | s.add_development_dependency 'rspec-rails' 24 | s.add_development_dependency 'pry' 25 | 26 | s.add_dependency "opal-rails" 27 | s.add_dependency "opal-browser" 28 | s.add_dependency 'react-rails' 29 | s.add_dependency 'therubyracer' 30 | s.add_dependency 'reactrb' 31 | 32 | end 33 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/reactive_record/engine', __FILE__) 6 | 7 | require 'rails/all' 8 | require 'rails/engine/commands' 9 | -------------------------------------------------------------------------------- /spec/server_unit_tests/pry_rescue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "ServerDataCache" do 4 | 5 | before(:all) do 6 | @current_pry_definition = Object.const_get("Pry") if defined? Pry 7 | @current_pry_rescue_definition = Object.const_get("PryRescue") if defined? PryRescue 8 | end 9 | 10 | after(:all) do 11 | Object.const_set("Pry", @current_pry_definition) if @current_pry_definition 12 | Object.const_set("PryRescue", @current_pry_definition) if @current_pry_rescue_definition 13 | end 14 | 15 | it "behaves normally if there is no pry rescue" do 16 | binding.pry 17 | expect(ReactiveRecord::ServerDataCache[[],[], ["User", ["new", 10852], "fake_attribute"], nil]).to raise_error 18 | end 19 | 20 | context "will use pry rescue if it is defined" do 21 | 22 | before(:all) do 23 | dummy_pry = Class.new do 24 | def self.rescue 25 | yield 26 | end 27 | def self.rescued(e) 28 | @last_exception = e 29 | end 30 | def self.last_exception 31 | @last_exception 32 | end 33 | end 34 | Object.const_set("PryRescue", true) 35 | Object.const_set("Pry", dummy_pry) 36 | end 37 | 38 | it "and it will still raise an error" do 39 | expect(ReactiveRecord::ServerDataCache[[],[], ["User", ["new", 10852], "fake_attribute"], nil]).to raise_error 40 | end 41 | 42 | it "and it will call Pry.rescued" do 43 | ReactiveRecord::ServerDataCache[[],[], ["User", ["new", 10852], "fake_attribute"], nil] rescue nil 44 | expect(dummy_pry.last_exception).to be_a(Exception) 45 | end 46 | 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /spec/test_app/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gem "rails" 3 | gem 'sqlite3' 4 | gem 'react-rails', "1.3.3" 5 | gem 'reactrb' 6 | gem 'opal-rails', git: "https://github.com/reactrb/opal-rails.git" 7 | gem 'reactive-record', path: "../.." 8 | gem 'jquery-cookie-rails' 9 | gem 'byebug' 10 | gem 'pry-rails' 11 | gem 'pry-rescue' 12 | gem 'opal-rspec-rails', git: 'https://github.com/reactrb/opal-rspec-rails.git' 13 | -------------------------------------------------------------------------------- /spec/test_app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/reactrb/opal-rails.git 3 | revision: 1549552d45996a3bc966403cdb2a527e262b77a8 4 | specs: 5 | opal-rails (0.9.0.dev) 6 | jquery-rails 7 | opal (>= 0.8.0, < 0.10) 8 | opal-activesupport (>= 0.0.5) 9 | opal-jquery (~> 0.4.0) 10 | rails (>= 4.0, < 6.0) 11 | sprockets-rails (< 3.0) 12 | 13 | GIT 14 | remote: https://github.com/reactrb/opal-rspec-rails.git 15 | revision: 2c921c4749b3c986d74d4d0c706a84c7f8ef3f21 16 | specs: 17 | opal-rspec-rails (0.1.0) 18 | opal (>= 0.8) 19 | opal-rails (~> 0.9.0.dev) 20 | opal-rspec (~> 0.5.0) 21 | 22 | PATH 23 | remote: ../.. 24 | specs: 25 | reactive-record (0.9.0) 26 | opal-browser 27 | opal-rails 28 | rails (>= 3.2.13) 29 | react-rails 30 | reactrb 31 | therubyracer 32 | 33 | GEM 34 | remote: http://rubygems.org/ 35 | specs: 36 | actionmailer (4.2.6) 37 | actionpack (= 4.2.6) 38 | actionview (= 4.2.6) 39 | activejob (= 4.2.6) 40 | mail (~> 2.5, >= 2.5.4) 41 | rails-dom-testing (~> 1.0, >= 1.0.5) 42 | actionpack (4.2.6) 43 | actionview (= 4.2.6) 44 | activesupport (= 4.2.6) 45 | rack (~> 1.6) 46 | rack-test (~> 0.6.2) 47 | rails-dom-testing (~> 1.0, >= 1.0.5) 48 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 49 | actionview (4.2.6) 50 | activesupport (= 4.2.6) 51 | builder (~> 3.1) 52 | erubis (~> 2.7.0) 53 | rails-dom-testing (~> 1.0, >= 1.0.5) 54 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 55 | activejob (4.2.6) 56 | activesupport (= 4.2.6) 57 | globalid (>= 0.3.0) 58 | activemodel (4.2.6) 59 | activesupport (= 4.2.6) 60 | builder (~> 3.1) 61 | activerecord (4.2.6) 62 | activemodel (= 4.2.6) 63 | activesupport (= 4.2.6) 64 | arel (~> 6.0) 65 | activesupport (4.2.6) 66 | i18n (~> 0.7) 67 | json (~> 1.7, >= 1.7.7) 68 | minitest (~> 5.1) 69 | thread_safe (~> 0.3, >= 0.3.4) 70 | tzinfo (~> 1.1) 71 | arel (6.0.3) 72 | babel-source (5.8.35) 73 | babel-transpiler (0.7.0) 74 | babel-source (>= 4.0, < 6) 75 | execjs (~> 2.0) 76 | builder (3.2.2) 77 | byebug (9.0.5) 78 | coderay (1.1.1) 79 | coffee-script-source (1.10.0) 80 | concurrent-ruby (1.0.2) 81 | connection_pool (2.2.0) 82 | erubis (2.7.0) 83 | execjs (2.7.0) 84 | globalid (0.3.6) 85 | activesupport (>= 4.1.0) 86 | hike (1.2.3) 87 | i18n (0.7.0) 88 | interception (0.5) 89 | jquery-cookie-rails (1.3.1.1) 90 | railties (>= 3.2.0, < 5.0) 91 | jquery-rails (4.1.1) 92 | rails-dom-testing (>= 1, < 3) 93 | railties (>= 4.2.0) 94 | thor (>= 0.14, < 2.0) 95 | json (1.8.3) 96 | libv8 (3.16.14.15) 97 | loofah (2.0.3) 98 | nokogiri (>= 1.5.9) 99 | mail (2.6.4) 100 | mime-types (>= 1.16, < 4) 101 | method_source (0.8.2) 102 | mime-types (3.1) 103 | mime-types-data (~> 3.2015) 104 | mime-types-data (3.2016.0521) 105 | mini_portile2 (2.1.0) 106 | minitest (5.9.0) 107 | nokogiri (1.6.8) 108 | mini_portile2 (~> 2.1.0) 109 | pkg-config (~> 1.1.7) 110 | opal (0.9.2) 111 | hike (~> 1.2) 112 | sourcemap (~> 0.1.0) 113 | sprockets (~> 3.1) 114 | tilt (>= 1.4) 115 | opal-activesupport (0.3.0) 116 | opal (>= 0.5.0, < 1.0.0) 117 | opal-browser (0.2.0) 118 | opal 119 | paggio 120 | opal-jquery (0.4.1) 121 | opal (>= 0.7.0, < 0.10.0) 122 | opal-rspec (0.5.0) 123 | opal (>= 0.8.0, < 0.10) 124 | paggio (0.2.6) 125 | pkg-config (1.1.7) 126 | pry (0.10.3) 127 | coderay (~> 1.1.0) 128 | method_source (~> 0.8.1) 129 | slop (~> 3.4) 130 | pry-rails (0.3.4) 131 | pry (>= 0.9.10) 132 | pry-rescue (1.4.4) 133 | interception (>= 0.5) 134 | pry 135 | rack (1.6.4) 136 | rack-test (0.6.3) 137 | rack (>= 1.0) 138 | rails (4.2.6) 139 | actionmailer (= 4.2.6) 140 | actionpack (= 4.2.6) 141 | actionview (= 4.2.6) 142 | activejob (= 4.2.6) 143 | activemodel (= 4.2.6) 144 | activerecord (= 4.2.6) 145 | activesupport (= 4.2.6) 146 | bundler (>= 1.3.0, < 2.0) 147 | railties (= 4.2.6) 148 | sprockets-rails 149 | rails-deprecated_sanitizer (1.0.3) 150 | activesupport (>= 4.2.0.alpha) 151 | rails-dom-testing (1.0.7) 152 | activesupport (>= 4.2.0.beta, < 5.0) 153 | nokogiri (~> 1.6.0) 154 | rails-deprecated_sanitizer (>= 1.0.1) 155 | rails-html-sanitizer (1.0.3) 156 | loofah (~> 2.0) 157 | railties (4.2.6) 158 | actionpack (= 4.2.6) 159 | activesupport (= 4.2.6) 160 | rake (>= 0.8.7) 161 | thor (>= 0.18.1, < 2.0) 162 | rake (11.1.2) 163 | react-rails (1.3.3) 164 | babel-transpiler (>= 0.7.0) 165 | coffee-script-source (~> 1.8) 166 | connection_pool 167 | execjs 168 | rails (>= 3.2) 169 | tilt 170 | reactrb (0.8.1) 171 | opal (>= 0.8.0) 172 | opal-activesupport (>= 0.2.0) 173 | opal-browser (= 0.2.0) 174 | ref (2.0.0) 175 | slop (3.6.0) 176 | sourcemap (0.1.1) 177 | sprockets (3.6.0) 178 | concurrent-ruby (~> 1.0) 179 | rack (> 1, < 3) 180 | sprockets-rails (2.3.3) 181 | actionpack (>= 3.0) 182 | activesupport (>= 3.0) 183 | sprockets (>= 2.8, < 4.0) 184 | sqlite3 (1.3.11) 185 | therubyracer (0.12.2) 186 | libv8 (~> 3.16.14.0) 187 | ref 188 | thor (0.19.1) 189 | thread_safe (0.3.5) 190 | tilt (2.0.5) 191 | tzinfo (1.2.2) 192 | thread_safe (~> 0.1) 193 | 194 | PLATFORMS 195 | ruby 196 | 197 | DEPENDENCIES 198 | byebug 199 | jquery-cookie-rails 200 | opal-rails! 201 | opal-rspec-rails! 202 | pry-rails 203 | pry-rescue 204 | rails 205 | react-rails (= 1.3.3) 206 | reactive-record! 207 | reactrb 208 | sqlite3 209 | 210 | BUNDLED WITH 211 | 1.10.6 212 | -------------------------------------------------------------------------------- /spec/test_app/README.rdoc: -------------------------------------------------------------------------------- 1 | == Welcome to Rails 2 | 3 | Rails is a web-application framework that includes everything needed to create 4 | database-backed web applications according to the Model-View-Control pattern. 5 | 6 | This pattern splits the view (also called the presentation) into "dumb" 7 | templates that are primarily responsible for inserting pre-built data in between 8 | HTML tags. The model contains the "smart" domain objects (such as Account, 9 | Product, Person, Post) that holds all the business logic and knows how to 10 | persist themselves to a database. The controller handles the incoming requests 11 | (such as Save New Account, Update Product, Show Post) by manipulating the model 12 | and directing data to the view. 13 | 14 | In Rails, the model is handled by what's called an object-relational mapping 15 | layer entitled Active Record. This layer allows you to present the data from 16 | database rows as objects and embellish these data objects with business logic 17 | methods. You can read more about Active Record in 18 | link:files/vendor/rails/activerecord/README.html. 19 | 20 | The controller and view are handled by the Action Pack, which handles both 21 | layers by its two parts: Action View and Action Controller. These two layers 22 | are bundled in a single package due to their heavy interdependence. This is 23 | unlike the relationship between the Active Record and Action Pack that is much 24 | more separate. Each of these packages can be used independently outside of 25 | Rails. You can read more about Action Pack in 26 | link:files/vendor/rails/actionpack/README.html. 27 | 28 | 29 | == Getting Started 30 | 31 | 1. At the command prompt, create a new Rails application: 32 | rails new myapp (where myapp is the application name) 33 | 34 | 2. Change directory to myapp and start the web server: 35 | cd myapp; rails server (run with --help for options) 36 | 37 | 3. Go to http://localhost:3000/ and you'll see: 38 | "Welcome aboard: You're riding Ruby on Rails!" 39 | 40 | 4. Follow the guidelines to start developing your application. You can find 41 | the following resources handy: 42 | 43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html 44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ 45 | 46 | 47 | == Debugging Rails 48 | 49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that 50 | will help you debug it and get it back on the rails. 51 | 52 | First area to check is the application log files. Have "tail -f" commands 53 | running on the server.log and development.log. Rails will automatically display 54 | debugging and runtime information to these files. Debugging info will also be 55 | shown in the browser on requests from 127.0.0.1. 56 | 57 | You can also log your own messages directly into the log file from your code 58 | using the Ruby logger class from inside your controllers. Example: 59 | 60 | class WeblogController < ActionController::Base 61 | def destroy 62 | @weblog = Weblog.find(params[:id]) 63 | @weblog.destroy 64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") 65 | end 66 | end 67 | 68 | The result will be a message in your log file along the lines of: 69 | 70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! 71 | 72 | More information on how to use the logger is at http://www.ruby-doc.org/core/ 73 | 74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are 75 | several books available online as well: 76 | 77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) 78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) 79 | 80 | These two books will bring you up to speed on the Ruby language and also on 81 | programming in general. 82 | 83 | 84 | == Debugger 85 | 86 | Debugger support is available through the debugger command when you start your 87 | Mongrel or WEBrick server with --debugger. This means that you can break out of 88 | execution at any point in the code, investigate and change the model, and then, 89 | resume execution! You need to install ruby-debug to run the server in debugging 90 | mode. With gems, use sudo gem install ruby-debug. Example: 91 | 92 | class WeblogController < ActionController::Base 93 | def index 94 | @posts = Post.all 95 | debugger 96 | end 97 | end 98 | 99 | So the controller will accept the action, run the first line, then present you 100 | with a IRB prompt in the server window. Here you can do things like: 101 | 102 | >> @posts.inspect 103 | => "[#nil, "body"=>nil, "id"=>"1"}>, 105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" 107 | >> @posts.first.title = "hello from a debugger" 108 | => "hello from a debugger" 109 | 110 | ...and even better, you can examine how your runtime objects actually work: 111 | 112 | >> f = @posts.first 113 | => #nil, "body"=>nil, "id"=>"1"}> 114 | >> f. 115 | Display all 152 possibilities? (y or n) 116 | 117 | Finally, when you're ready to resume execution, you can enter "cont". 118 | 119 | 120 | == Console 121 | 122 | The console is a Ruby shell, which allows you to interact with your 123 | application's domain model. Here you'll have all parts of the application 124 | configured, just like it is when the application is running. You can inspect 125 | domain models, change values, and save to the database. Starting the script 126 | without arguments will launch it in the development environment. 127 | 128 | To start the console, run rails console from the application 129 | directory. 130 | 131 | Options: 132 | 133 | * Passing the -s, --sandbox argument will rollback any modifications 134 | made to the database. 135 | * Passing an environment name as an argument will load the corresponding 136 | environment. Example: rails console production. 137 | 138 | To reload your controllers and models after launching the console run 139 | reload! 140 | 141 | More information about irb can be found at: 142 | link:http://www.rubycentral.org/pickaxe/irb.html 143 | 144 | 145 | == dbconsole 146 | 147 | You can go to the command line of your database directly through rails 148 | dbconsole. You would be connected to the database with the credentials 149 | defined in database.yml. Starting the script without arguments will connect you 150 | to the development database. Passing an argument will connect you to a different 151 | database, like rails dbconsole production. Currently works for MySQL, 152 | PostgreSQL and SQLite 3. 153 | 154 | == Description of Contents 155 | 156 | The default directory structure of a generated Ruby on Rails application: 157 | 158 | |-- app 159 | | |-- assets 160 | | | |-- images 161 | | | |-- javascripts 162 | | | `-- stylesheets 163 | | |-- controllers 164 | | |-- helpers 165 | | |-- mailers 166 | | |-- models 167 | | `-- views 168 | | `-- layouts 169 | |-- config 170 | | |-- environments 171 | | |-- initializers 172 | | `-- locales 173 | |-- db 174 | |-- doc 175 | |-- lib 176 | | |-- assets 177 | | `-- tasks 178 | |-- log 179 | |-- public 180 | |-- script 181 | |-- test 182 | | |-- fixtures 183 | | |-- functional 184 | | |-- integration 185 | | |-- performance 186 | | `-- unit 187 | |-- tmp 188 | | `-- cache 189 | | `-- assets 190 | `-- vendor 191 | |-- assets 192 | | |-- javascripts 193 | | `-- stylesheets 194 | `-- plugins 195 | 196 | app 197 | Holds all the code that's specific to this particular application. 198 | 199 | app/assets 200 | Contains subdirectories for images, stylesheets, and JavaScript files. 201 | 202 | app/controllers 203 | Holds controllers that should be named like weblogs_controller.rb for 204 | automated URL mapping. All controllers should descend from 205 | ApplicationController which itself descends from ActionController::Base. 206 | 207 | app/models 208 | Holds models that should be named like post.rb. Models descend from 209 | ActiveRecord::Base by default. 210 | 211 | app/views 212 | Holds the template files for the view that should be named like 213 | weblogs/index.html.erb for the WeblogsController#index action. All views use 214 | eRuby syntax by default. 215 | 216 | app/views/layouts 217 | Holds the template files for layouts to be used with views. This models the 218 | common header/footer method of wrapping views. In your views, define a layout 219 | using the layout :default and create a file named default.html.erb. 220 | Inside default.html.erb, call <% yield %> to render the view using this 221 | layout. 222 | 223 | app/helpers 224 | Holds view helpers that should be named like weblogs_helper.rb. These are 225 | generated for you automatically when using generators for controllers. 226 | Helpers can be used to wrap functionality for your views into methods. 227 | 228 | config 229 | Configuration files for the Rails environment, the routing map, the database, 230 | and other dependencies. 231 | 232 | db 233 | Contains the database schema in schema.rb. db/migrate contains all the 234 | sequence of Migrations for your schema. 235 | 236 | doc 237 | This directory is where your application documentation will be stored when 238 | generated using rake doc:app 239 | 240 | lib 241 | Application specific libraries. Basically, any kind of custom code that 242 | doesn't belong under controllers, models, or helpers. This directory is in 243 | the load path. 244 | 245 | public 246 | The directory available for the web server. Also contains the dispatchers and the 247 | default HTML files. This should be set as the DOCUMENT_ROOT of your web 248 | server. 249 | 250 | script 251 | Helper scripts for automation and generation. 252 | 253 | test 254 | Unit and functional tests along with fixtures. When using the rails generate 255 | command, template test files will be generated for you and placed in this 256 | directory. 257 | 258 | vendor 259 | External libraries that the application depends on. Also includes the plugins 260 | subdirectory. If the app has frozen rails, those gems also go here, under 261 | vendor/rails/. This directory is in the load path. 262 | -------------------------------------------------------------------------------- /spec/test_app/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/application.rb: -------------------------------------------------------------------------------- 1 | require 'opal' 2 | require 'opal_ujs' 3 | #require 'react' 4 | require 'reactrb' 5 | # not sure why we are not requiring components 6 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/components/another_component.rb: -------------------------------------------------------------------------------- 1 | require 'user' 2 | class AnotherComponent 3 | 4 | include React::Component 5 | 6 | export_component 7 | 8 | required_param :user, type: User 9 | 10 | backtrace :on 11 | 12 | def render 13 | div do 14 | "#{user.name}'s todos:".br 15 | ul do 16 | broken! 17 | user.todo_items.each do |todo| 18 | li { TodoItemComponent(todo: todo) } 19 | end 20 | end 21 | end 22 | end 23 | 24 | end -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/components/empty_component.rb: -------------------------------------------------------------------------------- 1 | class EmptyComponent 2 | include React::Component 3 | export_component 4 | def render 5 | end 6 | end -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/components/todo_item_component.js.rb: -------------------------------------------------------------------------------- 1 | require 'reactrb' 2 | 3 | class TodoItemComponent 4 | 5 | include React::Component 6 | 7 | required_param :todo 8 | backtrace :on 9 | 10 | def render 11 | div do 12 | "Title: #{todo.title}".br; "Description #{todo.description}".br; "User #{todo.user.name}" 13 | end 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/components/todos_component.js.rb: -------------------------------------------------------------------------------- 1 | require 'reactrb' 2 | require 'user' 3 | require 'reactive-record' 4 | 5 | class TodosComponent 6 | 7 | include React::Component 8 | 9 | export_component 10 | 11 | #optional_param :initial_user_email 12 | required_param :users, type: [User] 13 | #define_state :users 14 | 15 | before_mount do 16 | # `debugger` 17 | nil 18 | #users! [User.find_by_id(1), User.find_by_id(2), User.find_by_id(3)] 19 | end 20 | 21 | after_mount do 22 | #puts "after mount" 23 | # `debugger` 24 | nil 25 | end 26 | 27 | backtrace :on 28 | 29 | after_update do 30 | #puts "after update" 31 | if user 32 | # `debugger` 33 | nil 34 | end 35 | end 36 | 37 | def render 38 | div do 39 | TodosMainComponent(users: users) 40 | end.hide_while_loading 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/components/todos_main_component.rb: -------------------------------------------------------------------------------- 1 | class TodosMainComponent 2 | 3 | include React::Component 4 | 5 | required_param :users 6 | 7 | define_state :user, :user_email 8 | 9 | before_mount do 10 | #user_email! "mitch@catprint.com" 11 | user! User.find_by_email(user_email) if user_email 12 | end 13 | 14 | def render 15 | div do 16 | if true 17 | table do 18 | tbody do 19 | tr { td {"name"}; td {"email"}; td {"number of todos"}} 20 | users.each do |user| 21 | tr {user.name.td; user.email.td; user.todo_items.count.td.while_loading("-") } 22 | end 23 | end 24 | end 25 | end 26 | div do 27 | "Todos for ".span 28 | input(type: :text, value: user_email, placeholder: "enter a user's email"). 29 | on(:change) { |e| user_email! e.target.value }. 30 | on(:key_up) { |e| user! User.find_by_email(user_email) if e.key_code == 13 } 31 | end 32 | if !user 33 | "type in an email and hit return to find a user" 34 | elsif user.not_found? 35 | "#{user.email} does not exist, try another email" 36 | elsif user.todo_items.count == 0 37 | "No Todos Yet" 38 | else 39 | div do 40 | user.todo_items.each do |todo| 41 | TodoItemComponent(todo: todo) 42 | end 43 | end.while_loading "searching..." 44 | end 45 | end 46 | 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/reactive_record_config.js: -------------------------------------------------------------------------------- 1 | 2 | window.ReactiveRecordEnginePath = "/rr" -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/spec/reactive_record_xspec.js.rb: -------------------------------------------------------------------------------- 1 | require 'spec/spec_helper' 2 | require 'user' 3 | 4 | 5 | 6 | describe "Reactive Record" do 7 | 8 | after(:each) { React::API.clear_component_class_cache } 9 | 10 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work 11 | 12 | # describe "a passing dummy test" do 13 | # it "passes" do 14 | # expect(true).to be(true) 15 | # end 16 | # end 17 | 18 | describe "reactive_record basic api" do 19 | 20 | rendering("a simple component") do 21 | div {"hello"} 22 | end.should_immediately_generate do |component| 23 | component.html == "hello" 24 | end 25 | 26 | rendering("a find_by query") do 27 | User.find_by_email("mitch@catprint.com").email 28 | end.should_immediately_generate do 29 | html == "mitch@catprint.com" 30 | end 31 | 32 | it "should yield the same find_by result if called twice" do 33 | ar1 = User.find_by_email("mitch@catprint.com") 34 | ar2 = User.find_by_email("mitch@catprint.com") 35 | expect(ar1.equal?(ar2)).to be(true) 36 | end 37 | 38 | 39 | end 40 | 41 | end 42 | 43 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /spec/test_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | #protect_from_forgery 3 | 4 | def acting_user 5 | cookies[:acting_user] and User.find_by_email(cookies[:acting_user]) 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /spec/test_app/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | 3 | def index 4 | redirect_to "/opal_spec" 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/test_app/app/controllers/test_controller.rb: -------------------------------------------------------------------------------- 1 | class TestController < ApplicationController 2 | 3 | def index 4 | render inline: "<%= react_component 'Test', {}, { prerender: !params[:no_prerender] } %>", layout: nil 5 | end 6 | 7 | end -------------------------------------------------------------------------------- /spec/test_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/test_app/app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catprintlabs/reactive-record/7de25cd8592a5c73e0c4e0721cf38f57251d94f1/spec/test_app/app/mailers/.gitkeep -------------------------------------------------------------------------------- /spec/test_app/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catprintlabs/reactive-record/7de25cd8592a5c73e0c4e0721cf38f57251d94f1/spec/test_app/app/models/.gitkeep -------------------------------------------------------------------------------- /spec/test_app/app/views/components.rb: -------------------------------------------------------------------------------- 1 | require 'opal' 2 | require 'reactive-record' 3 | require 'models' 4 | require_tree './components' 5 | 6 | -------------------------------------------------------------------------------- /spec/test_app/app/views/components/test.rb: -------------------------------------------------------------------------------- 1 | class Test 2 | 3 | include React::Component 4 | 5 | def render 6 | user = User.find_by_email("mitch@catprint.com") 7 | div do 8 | "#{Time.now.to_s} #{user.first_name}".br 9 | "zip: #{user.address.zip}".br 10 | "todos: #{user.todo_items.collect { |todo| todo.title }.join(", ")}".br 11 | "first todo in find_string(mitch) scope: #{user.todo_items.find_string("mitch").first.title}".br 12 | "a comment was made by: #{user.todo_items.first.commenters.first.email}".br 13 | "some expensive math: #{user.expensive_math(13)}".br 14 | "and a server side method: #{user.detailed_name}".br.tap { user.first_name = "joe"; user.detailed_name! } 15 | end 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/test_app/app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 | Hello there check the console -------------------------------------------------------------------------------- /spec/test_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <%= stylesheet_link_tag "application", :media => "all" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | 12 | <%= yield %> 13 | 14 | <%= javascript_include_tag "application" unless params[:no_js] %> 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/test_app/app/views/models.rb: -------------------------------------------------------------------------------- 1 | require_tree './models' 2 | -------------------------------------------------------------------------------- /spec/test_app/app/views/models/address.rb: -------------------------------------------------------------------------------- 1 | class Address < ActiveRecord::Base 2 | 3 | MAPPED_FIELDS = %w(id street city state zip) 4 | 5 | def self.compose(*args) 6 | new.tap do |address| 7 | MAPPED_FIELDS.each_with_index do |field_name, i| 8 | address.send("#{field_name}=", args[i]) 9 | end 10 | end 11 | end 12 | 13 | end -------------------------------------------------------------------------------- /spec/test_app/app/views/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | 3 | def create_permitted? 4 | # for testing we allow anything if there is no acting_user 5 | # in the real world you would have something like this: 6 | # acting_user and (acting_user.admin? or user_is? acting_user) 7 | !acting_user or user_is? acting_user 8 | end 9 | 10 | def destroy_permitted? 11 | !acting_user or user_is? acting_user 12 | end 13 | 14 | belongs_to :user 15 | belongs_to :todo_item 16 | 17 | has_one :todo, -> {}, class_name: "TodoItem" # this is just so we can test scopes params and null belongs_to relations 18 | 19 | end -------------------------------------------------------------------------------- /spec/test_app/app/views/models/todo_item.rb: -------------------------------------------------------------------------------- 1 | class TodoItem < ActiveRecord::Base 2 | 3 | def view_permitted?(attribute) 4 | !acting_user or user_is? acting_user 5 | end 6 | 7 | def update_permitted? 8 | return true unless acting_user 9 | return only_changed? :comments unless user_is? acting_user 10 | true 11 | end 12 | 13 | belongs_to :user 14 | has_many :comments 15 | has_many :commenters, class_name: "User", through: :comments, source: :user 16 | belongs_to :comment # just so we can test an empty belongs_to relationship 17 | 18 | scope :find_string, ->(s) { where("title LIKE ? OR description LIKE ?", "%#{s}%", "%#{s}%") } 19 | 20 | scope :active, -> { where("title LIKE '%mitch%' OR description LIKE '%mitch%'")} 21 | to_sync :active do |scope, record| 22 | if record.title =~ /mitch/ || record.description =~ /mitch/ 23 | scope << record 24 | else 25 | scope.delete(record) 26 | end 27 | end 28 | 29 | scope :important, -> { where("title LIKE '%another%' OR description LIKE '%another%'")} 30 | to_sync(:important) {title =~ /another/ || description =~ /another/ } 31 | 32 | def virtual_user_first_name 33 | user.first_name 34 | end unless RUBY_ENGINE == 'opal' 35 | 36 | end 37 | -------------------------------------------------------------------------------- /spec/test_app/app/views/models/user.rb: -------------------------------------------------------------------------------- 1 | class TestData 2 | 3 | def initialize(string, times) 4 | @string = string 5 | @times = times 6 | end 7 | 8 | attr_accessor :string 9 | attr_accessor :times 10 | 11 | def big_string 12 | puts "calling big_string #{string} * #{times}" 13 | string * times 14 | end 15 | 16 | end 17 | 18 | class User < ActiveRecord::Base 19 | 20 | def view_permitted?(attribute) 21 | return self == acting_user if acting_user 22 | super # we call super to test if its there (just for the spec) not really the right way to do it, see comments or todo_items 23 | end 24 | 25 | has_many :todo_items 26 | has_many :comments 27 | has_many :commented_on_items, class_name: "TodoItem", through: :comments, source: :todo_item 28 | 29 | composed_of :address, :class_name => 'Address', :constructor => :compose, :mapping => Address::MAPPED_FIELDS.map {|f| ["address_#{f}", f] } 30 | composed_of :address2, :class_name => 'Address', :constructor => :compose, :mapping => Address::MAPPED_FIELDS.map {|f| ["address2_#{f}", f] } 31 | 32 | composed_of :data, :class_name => 'TestData', :allow_nil => true, :mapping => [['data_string', 'string'], ['data_times', 'times']] 33 | 34 | enum test_enum: [:yes, :no] 35 | 36 | def name 37 | "#{first_name} #{last_name}" 38 | end 39 | 40 | # two examples of server side calculated attributes. The second takes a parameter. 41 | # the first does not rely on an id, so can be used before the record is saved. 42 | 43 | def detailed_name 44 | s = "#{first_name[0]}. #{last_name}" rescue "" 45 | s += " - #{email}" if email 46 | s += " (#{todo_items.size} todo#{'s' if todo_items.size > 1})" if todo_items.size > 0 47 | s 48 | end unless RUBY_ENGINE == 'opal' 49 | 50 | def expensive_math(n) 51 | n*n 52 | end unless RUBY_ENGINE == 'opal' 53 | 54 | # this is also used for remote calculation in the aggregate test 55 | 56 | def verify_zip 57 | if address.zip =~ /^\d{5}$/ 58 | address.zip 59 | end 60 | end unless RUBY_ENGINE == 'opal' 61 | 62 | end 63 | 64 | class User < ActiveRecord::Base 65 | 66 | def as_json(*args) 67 | {name: "bozo"} 68 | end 69 | 70 | validates :email, format: {with: /\@.+\./}, :allow_nil => true 71 | 72 | def name=(val) # this is here to test ability to save changes to this type of psuedo attribute 73 | val = val.split(" ") 74 | self.first_name = val[0] 75 | self.last_name = val[1] 76 | end 77 | 78 | end unless RUBY_ENGINE == 'opal' 79 | -------------------------------------------------------------------------------- /spec/test_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | if true 4 | require ::File.expand_path('../config/environment', __FILE__) 5 | run Dummy::Application 6 | 7 | else 8 | require 'bundler' 9 | Bundler.require 10 | 11 | require "opal/rspec" 12 | require "opal-jquery" 13 | 14 | Opal.append_path File.expand_path('../spec', __FILE__) 15 | 16 | sprockets_env = Opal::RSpec::SprocketsEnvironment.new rescue nil 17 | if sprockets_env 18 | run Opal::Server.new(sprockets: sprockets_env) { |s| 19 | s.main = 'opal/rspec/sprockets_runner' 20 | sprockets_env.add_spec_paths_to_sprockets 21 | s.debug = false 22 | s.index_path = 'spec/index.html.erb' 23 | } 24 | else 25 | run Opal::Server.new { |s| 26 | s.main = 'opal/rspec/sprockets_runner' 27 | s.append_path 'spec' 28 | #s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js")) 29 | s.debug = true 30 | s.index_path = 'spec/index.html.erb' 31 | } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/test_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "reactive-record" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Custom directories with classes and modules you want to be autoloadable. 15 | 16 | # Only load the plugins named here, in the order given (default is alphabetical). 17 | # :all can be used as a placeholder for all plugins not explicitly named. 18 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 19 | 20 | # Activate observers that should always be running. 21 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 22 | 23 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 24 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 25 | # config.time_zone = 'Central Time (US & Canada)' 26 | 27 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 28 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 29 | # config.i18n.default_locale = :de 30 | 31 | # Configure the default encoding used in templates for Ruby 1.9. 32 | config.encoding = "utf-8" 33 | 34 | # Configure sensitive parameters which will be filtered from the log file. 35 | config.filter_parameters += [:password] 36 | 37 | # Enable escaping HTML in JSON. 38 | config.active_support.escape_html_entities_in_json = true 39 | 40 | # Use SQL instead of Active Record's schema dumper when creating the database. 41 | # This is necessary if your schema can't be completely dumped by the schema dumper, 42 | # like if you have constraints or database-specific column types 43 | # config.active_record.schema_format = :sql 44 | 45 | # Enforce whitelist mode for mass assignment. 46 | # This will create an empty whitelist of attributes available for mass-assignment for all models 47 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 48 | # parameters by using an attr_accessible or attr_protected declaration. 49 | #config.active_record.whitelist_attributes = true 50 | 51 | # Enable the asset pipeline 52 | config.assets.enabled = true 53 | 54 | config.autoload_paths += %W(#{config.root}/app/views/models) 55 | 56 | # Version of your assets, change this if you want to expire all your assets 57 | config.assets.version = '1.0' 58 | 59 | # These are the available opal-rspec options with their default value: 60 | config.opal.method_missing = true 61 | config.opal.optimized_operators = true 62 | config.opal.arity_check = false 63 | config.opal.const_missing = true 64 | config.opal.dynamic_require_severity = :ignore 65 | 66 | # Enable/disable /opal_specs route 67 | config.opal.enable_specs = true 68 | 69 | config.opal.spec_location = 'spec-opal' 70 | end 71 | end 72 | 73 | -------------------------------------------------------------------------------- /spec/test_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /spec/test_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /spec/test_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/test_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise exception on mass assignment protection for Active Record models 26 | #config.active_record.mass_assignment_sanitizer = :strict 27 | 28 | # Log the query plan for queries taking more than this (works 29 | # with SQLite, MySQL, and PostgreSQL) 30 | #config.active_record.auto_explain_threshold_in_seconds = 0.5 31 | 32 | # Do not compress assets 33 | config.assets.compress = false 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = false #true 37 | 38 | config.eager_load = false 39 | 40 | config.react.variant = :development 41 | config.react.addons = true 42 | end 43 | -------------------------------------------------------------------------------- /spec/test_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /spec/test_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Raise exception on mass assignment protection for Active Record models 33 | #config.active_record.mass_assignment_sanitizer = :strict 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | end 38 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = '2fce5780e24de7608de5160433088226a6f908d516b8b2117e8bc7d7c25e63d2cae3b0f56f1cb40ec1b38cab2e2e6d111d0aa28609ca79f0431b9fe90294146c' 8 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /spec/test_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/test_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | root :to => "home#index" 4 | match 'test', :to => "test#index", via: :get 5 | mount ReactiveRecord::Engine => "/rr" 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/test_app/db/development.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catprintlabs/reactive-record/7de25cd8592a5c73e0c4e0721cf38f57251d94f1/spec/test_app/db/development.sqlite3 -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20150617002932_create_todo_items.rb: -------------------------------------------------------------------------------- 1 | class CreateTodoItems < ActiveRecord::Migration 2 | def change 3 | create_table :todo_items do |t| 4 | t.string :title 5 | t.text :description 6 | t.boolean :complete 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20150617134028_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :first_name 5 | t.string :last_name 6 | t.string :email 7 | 8 | t.timestamps 9 | end 10 | 11 | add_column :todo_items, :user_id, :integer 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20150729195556_add_address_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAddressToUser < ActiveRecord::Migration 2 | 3 | def change 4 | 5 | add_column :users, :address_street, :string 6 | add_column :users, :address_city, :string 7 | add_column :users, :address_state, :string 8 | add_column :users, :address_zip, :string 9 | 10 | create_table :addresses do |t| 11 | t.string :street 12 | t.string :city 13 | t.string :state 14 | t.string :zip 15 | t.timestamps 16 | end 17 | 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20150826142045_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration 2 | def change 3 | create_table :comments do |t| 4 | t.integer :user_id 5 | t.integer :todo_item_id 6 | t.string :comment 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20150828172008_add_single_comment_to_todo_item.rb: -------------------------------------------------------------------------------- 1 | class AddSingleCommentToTodoItem < ActiveRecord::Migration 2 | def change 3 | add_column :todo_items, :comment_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20150908184118_add_address_id_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAddressIdToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :address_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20150917220236_add_second_address_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddSecondAddressToUser < ActiveRecord::Migration 2 | def change 3 | 4 | add_column :users, :address2_street, :string 5 | add_column :users, :address2_city, :string 6 | add_column :users, :address2_state, :string 7 | add_column :users, :address2_zip, :string 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20151009000111_add_test_data_attributes_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddTestDataAttributesToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :data_string, :string 4 | add_column :users, :data_times, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/test_app/db/migrate/20160129182544_add_test_enum_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddTestEnumToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :test_enum, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/test_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20160129182544) do 15 | 16 | create_table "addresses", force: :cascade do |t| 17 | t.string "street" 18 | t.string "city" 19 | t.string "state" 20 | t.string "zip" 21 | t.datetime "created_at", null: false 22 | t.datetime "updated_at", null: false 23 | end 24 | 25 | create_table "comments", force: :cascade do |t| 26 | t.integer "user_id" 27 | t.integer "todo_item_id" 28 | t.string "comment" 29 | t.datetime "created_at" 30 | t.datetime "updated_at" 31 | end 32 | 33 | create_table "todo_items", force: :cascade do |t| 34 | t.string "title" 35 | t.text "description" 36 | t.boolean "complete" 37 | t.datetime "created_at", null: false 38 | t.datetime "updated_at", null: false 39 | t.integer "user_id" 40 | t.integer "comment_id" 41 | end 42 | 43 | create_table "users", force: :cascade do |t| 44 | t.string "first_name" 45 | t.string "last_name" 46 | t.string "email" 47 | t.datetime "created_at", null: false 48 | t.datetime "updated_at", null: false 49 | t.string "address_street" 50 | t.string "address_city" 51 | t.string "address_state" 52 | t.string "address_zip" 53 | t.integer "address_id" 54 | t.string "address2_street" 55 | t.string "address2_city" 56 | t.string "address2_state" 57 | t.string "address2_zip" 58 | t.string "data_string" 59 | t.integer "data_times" 60 | t.integer "test_enum" 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /spec/test_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | users = [ 2 | ["Mitch", "VanDuyn", "mitch@catprint.com"], 3 | ["Todd", "Russell", "todd@catprint.com"], 4 | ["Adam", "George", "adamg@catprint.com"], 5 | ["Test1", "Test1", "test1@catprint.com"] 6 | ] 7 | 8 | users.each do |first_name, last_name, email| 9 | User.create({ 10 | first_name: first_name, last_name: last_name, email: email, 11 | address_street: "4348 Culver Road", address_city: "Rochester", address_state: "NY", address_zip: "14617" 12 | } 13 | #without_protection: true 14 | ) 15 | end 16 | 17 | todo_items = [ 18 | { 19 | title: "a todo for mitch", 20 | description: "mitch has a big fat todo to do!", 21 | user: User.find_by_email("mitch@catprint.com"), 22 | comments: [{user: User.find_by_email("adamg@catprint.com"), comment: "get it done mitch"}] 23 | }, 24 | { 25 | title: "another todo for mitch", 26 | description: "mitch has too many todos", 27 | user: User.find_by_email("mitch@catprint.com") 28 | }, 29 | { 30 | title: "do it again Todd", 31 | description: "Todd please do that great thing you did again", 32 | user: User.find_by_email("todd@catprint.com") 33 | }, 34 | { 35 | title: "no user todo", 36 | description: "the description" 37 | }, 38 | { 39 | title: "test 1 todo 1", description: "test 1 todo 1", user: User.find_by_email("test1@catprint.com"), 40 | comments: [ 41 | {user: User.find_by_email("mitch@catprint.com"), comment: "test 1 todo 1 comment 1"}, 42 | {user: User.find_by_email("mitch@catprint.com"), comment: "test 1 todo 1 comment 2"} 43 | ] 44 | }, 45 | { 46 | title: "test 1 todo 2", description: "test 1 todo 2", user: User.find_by_email("test1@catprint.com"), 47 | comments: [ 48 | {user: User.find_by_email("mitch@catprint.com"), comment: "test 1 todo 2 comment 1"}, 49 | {user: User.find_by_email("mitch@catprint.com"), comment: "test 1 todo 2 comment 2"} 50 | ] 51 | } 52 | ] 53 | 54 | todo_items.each do |attributes| 55 | comments = attributes.delete(:comments) || [] 56 | todo = TodoItem.create(attributes) #, without_protection: true) 57 | comments.each do |attributes| 58 | Comment.create(attributes.merge(todo_item: todo)) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/test_app/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catprintlabs/reactive-record/7de25cd8592a5c73e0c4e0721cf38f57251d94f1/spec/test_app/db/test.sqlite3 -------------------------------------------------------------------------------- /spec/test_app/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catprintlabs/reactive-record/7de25cd8592a5c73e0c4e0721cf38f57251d94f1/spec/test_app/log/.gitkeep -------------------------------------------------------------------------------- /spec/test_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/test_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/test_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /spec/test_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catprintlabs/reactive-record/7de25cd8592a5c73e0c4e0721cf38f57251d94f1/spec/test_app/public/favicon.ico -------------------------------------------------------------------------------- /spec/test_app/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/aggregations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | #require 'active_record' 3 | #Opal::RSpec::Runner.autorun 4 | 5 | class Thing < ActiveRecord::Base 6 | end 7 | 8 | class ThingContainer < ActiveRecord::Base 9 | composed_of :thing 10 | composed_of :another_thing, :class_name => Thing 11 | end 12 | 13 | 14 | describe "ActiveRecord" do 15 | after(:each) { React::API.clear_component_class_cache } 16 | 17 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work 18 | 19 | # describe "a passing dummy test" do 20 | # it "passes" do 21 | # expect(true).to be(true) 22 | # end 23 | # end 24 | 25 | describe "Aggregation Reflection" do 26 | 27 | it "knows the aggregates class" do 28 | expect(ThingContainer.reflect_on_aggregation(:thing).klass).to eq(Thing) 29 | end 30 | 31 | it "knows the aggregates attribute" do 32 | expect(ThingContainer.reflect_on_aggregation(:thing).attribute).to eq(:thing) 33 | end 34 | 35 | it "knows all the Aggregates" do 36 | expect(ThingContainer.reflect_on_all_aggregations.count).to eq(2) 37 | end 38 | 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/associations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | #require 'user' 3 | #require 'todo_item' 4 | 5 | 6 | class Thing < ActiveRecord::Base 7 | belongs_to :bucket 8 | end 9 | 10 | class Bucket < ActiveRecord::Base 11 | has_many :things 12 | end 13 | 14 | class OtherThing < ActiveRecord::Base 15 | has_many :things, through: :thing_group 16 | end 17 | 18 | describe "ActiveRecord" do 19 | 20 | after(:each) { React::API.clear_component_class_cache } 21 | 22 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work 23 | 24 | # describe "a passing dummy test" do 25 | # it "passes" do 26 | # expect(true).to be(true) 27 | # end 28 | # end 29 | 30 | 31 | describe "Association Reflection" do 32 | 33 | it "knows the foreign key of a belongs_to relationship" do 34 | expect(Thing.reflect_on_association(:bucket).association_foreign_key).to eq(:bucket_id) 35 | end 36 | 37 | it "knows the foreign key of a has_many relationship" do 38 | expect(Bucket.reflect_on_association(:things).association_foreign_key).to eq(:bucket_id) 39 | end 40 | 41 | it "knows the attribute name" do 42 | expect(Bucket.reflect_on_association(:things).attribute).to eq(:things) 43 | end 44 | 45 | it "knows the associated klass" do 46 | expect(Bucket.reflect_on_association(:things).klass).to eq(Thing) 47 | end 48 | 49 | it "knows the macro" do 50 | expect(Bucket.reflect_on_association(:things).macro).to eq(:has_many) 51 | end 52 | 53 | it "knows the inverse" do 54 | expect(Bucket.reflect_on_association(:things).inverse_of).to eq(:bucket) 55 | end 56 | 57 | it "knows if the association is a collection" do 58 | expect(Bucket.reflect_on_association(:things).collection?).to be_truthy 59 | end 60 | 61 | it "knows if the association is not a collection" do 62 | expect(Thing.reflect_on_association(:bucket).collection?).to be_falsy 63 | end 64 | 65 | it "knows the associated klass of a has_many_through relationship" do 66 | expect(OtherThing.reflect_on_association(:things).klass).to eq(Thing) 67 | end 68 | 69 | it "knows a has_many_through is a collection" do 70 | expect(OtherThing.reflect_on_association(:things).collection?).to be_truthy 71 | end 72 | 73 | it "does not return a inverse for a has_many_through collection" do 74 | expect(OtherThing.reflect_on_association(:things).inverse_of).to be_nil 75 | end 76 | 77 | end 78 | 79 | end -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | #Opal::RSpec::Runner.autorun 3 | 4 | class BaseClass < ActiveRecord::Base 5 | end 6 | 7 | class SubClass < BaseClass 8 | end 9 | 10 | class Funky < ActiveRecord::Base 11 | self.primary_key = :funky_id 12 | self.inheritance_column = :funky_type 13 | end 14 | 15 | class BelongsTo < ActiveRecord::Base 16 | belongs_to :has_many 17 | belongs_to :has_one 18 | belongs_to :best_friend, class_name: "HasMany", foreign_key: :bf_id 19 | end 20 | 21 | class HasMany < ActiveRecord::Base 22 | has_many :belongs_to 23 | has_many :best_friends, class_name: "BelongsTo", foreign_key: :bf_id 24 | end 25 | 26 | class HasOne < ActiveRecord::Base 27 | has_one :belongs_to 28 | end 29 | 30 | class Scoped < ActiveRecord::Base 31 | scope :only_those_guys 32 | end 33 | 34 | describe "ActiveRecord" do 35 | 36 | before(:all) { React::IsomorphicHelpers.load_context } 37 | 38 | after(:each) { React::API.clear_component_class_cache } 39 | 40 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work 41 | 42 | # describe "a passing dummy test" do 43 | # it "passes" do 44 | # expect(true).to be(true) 45 | # end 46 | # end 47 | 48 | describe "reactive_record active_record base methods" do 49 | 50 | it "will find the base class" do 51 | expect(SubClass.base_class).to eq(BaseClass) 52 | end 53 | 54 | it "knows the primary key" do 55 | expect(BaseClass.primary_key).to eq(:id) 56 | end 57 | 58 | it "can override the primary key" do 59 | expect(Funky.primary_key).to eq(:funky_id) 60 | end 61 | 62 | it "knows the inheritance column" do 63 | expect(BaseClass.inheritance_column).to eq(:type) 64 | end 65 | 66 | it "can override the inheritance column" do 67 | expect(Funky.inheritance_column).to eq(:funky_type) 68 | end 69 | 70 | it "knows the model name" do 71 | expect(BaseClass.model_name).to eq("BaseClass") 72 | end 73 | 74 | it "can find a record by id" do 75 | expect(BaseClass.find(12).id).to eq(12) 76 | end 77 | 78 | it "has a find_by_xxx method" do 79 | expect(BaseClass.find_by_xxx("beer").xxx).to eq("beer") 80 | end 81 | 82 | it "will correctly infer the model type from the inheritance column" do 83 | expect(BaseClass.find_by_type("SubClass").class).to eq(SubClass) 84 | expect(BaseClass.find_by_type(nil).class).to eq(BaseClass) 85 | end 86 | 87 | it "can have a has_many association" do 88 | expect(HasMany.reflect_on_association(:belongs_to).klass.reflect_on_association(:has_many).klass).to eq(HasMany) 89 | end 90 | 91 | it "can have a has_one association" do 92 | expect(HasOne.reflect_on_association(:belongs_to).klass.reflect_on_association(:has_one).klass).to eq(HasOne) 93 | end 94 | 95 | it "can override the class and foreign_key values when creating an association" do 96 | reflection = HasMany.reflect_on_association(:best_friends) 97 | expect(reflection.klass).to eq(BelongsTo) 98 | expect(reflection.association_foreign_key).to eq(:bf_id) 99 | end 100 | 101 | it "can have a scoping method" do 102 | expect(Scoped.only_those_guys.respond_to? :all).to be_truthy 103 | end 104 | 105 | it "can type check parameters" do 106 | expect(SubClass._react_param_conversion({attr1: 1, attr2: 2, type: "SubClass", id: 123}.to_n, :validate_only)).to be(true) 107 | end 108 | 109 | it "can type check parameters with native wrappers" do 110 | expect(SubClass._react_param_conversion(Native({attr1: 1, attr2: 2, type: "SubClass", id: 123}.to_n), :validate_only)).to be(true) 111 | end 112 | 113 | it "will fail type checking if type does not match" do 114 | expect(SubClass._react_param_conversion({attr1: 1, attr2: 2, type: nil, id: 123}.to_n, :validate_only)).to be_falsy 115 | end 116 | 117 | it "will convert a hash to an instance" do 118 | ar = SubClass._react_param_conversion({attr1: 1, attr2: 2, type: "SubClass", id: 123}.to_n) 119 | expect(ar.attr1).to eq(1) 120 | expect(ar.attr2).to eq(2) 121 | expect(ar.id).to eq(123) 122 | end 123 | 124 | end 125 | 126 | end 127 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/dummy_value_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "dummy values" do 4 | 5 | before(:each) { React::IsomorphicHelpers.load_context } 6 | 7 | it "fetches a dummy value" do 8 | expect(User.find_by_email("mitch@catprint.com").first_name.to_s.is_a?(String)).to be_truthy 9 | end 10 | 11 | it "can convert the value to a float" do 12 | expect(User.find_by_email("mitch@catprint.com").id.to_f.is_a?(Float)).to be_truthy 13 | end 14 | 15 | it "can convert the value to an int" do 16 | expect(User.find_by_email("mitch@catprint.com").id.to_i.is_a?(Integer)).to be_truthy 17 | end 18 | 19 | it "can do math on a value" do 20 | expect(1 + User.find_by_email("mitch@catprint.com").id).to eq(1) 21 | end 22 | 23 | xit "can do string things as well" do # can't because of the way strings work in opal 24 | expect("id: "+ User.find_by_email("mitch@catprint.com").id).to eq("id: ") 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/edge_cases_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "pending edge cases" do 4 | 5 | before(:each) do 6 | React::IsomorphicHelpers.load_context 7 | end 8 | 9 | it "base and subclass both belong to same parent record" 10 | it "will set changed on the parent record when updating a child aggregate" 11 | 12 | it "knows a targets owner before loading" do 13 | React::IsomorphicHelpers.load_context 14 | expect(User.find_by_email("mitch@catprint.com").todo_items.first.user.email).to eq("mitch@catprint.com") 15 | end 16 | 17 | it "can return a nil association" do 18 | ReactiveRecord.load do 19 | TodoItem.all.collect do |todo| 20 | todo.comment and todo.comment.comment 21 | end.compact 22 | end.then do |collection| 23 | expect(collection).to be_empty 24 | end 25 | end 26 | 27 | it "trims the association tree" do 28 | ReactiveRecord.load do 29 | TodoItem.all.collect do |todo| 30 | todo.user && todo.user.first_name 31 | end.compact 32 | end.then do |first_names| 33 | expect(first_names.count).to eq(5) 34 | end 35 | end 36 | 37 | async "reruns loads in order" do 38 | User.find_by_email("mitch@catprint.com").last_name 39 | after(0.01) do 40 | ReactiveRecord.load do 41 | TodoItem.find_by_title("do it again Todd").description 42 | end.then do |description| 43 | async { expect(description).to eq("Todd please do that great thing you did again") } 44 | end 45 | end 46 | end 47 | 48 | async "will load the same record via two different methods" do 49 | ReactiveRecord.load do 50 | # first load a record one way 51 | # on load retry we want to just insure the contents are loaded, but we are still pointing the same instance 52 | @r1 ||= User.find_by_email("mitch@catprint.com") 53 | @r1.address.zip # just so we grab something that is not the id 54 | @r1 55 | end.then do |r1| 56 | ReactiveRecord.load do 57 | # now repeat but get teh record a different way, this will return a different instance 58 | @r2 ||= User.find_by_first_name("Mitch") 59 | @r2.last_name # lets get the last name, when loaded the two record ids will match and will be merged 60 | @r2 61 | end.then do |r2| 62 | async do 63 | expect(r1.last_name).to eq(r2.last_name) 64 | expect(r1).to eq(r2) 65 | expect(r1).not_to be(r2) 66 | end 67 | end 68 | end 69 | end 70 | 71 | async "will load the same record via two different methods via a collection" do 72 | ReactiveRecord.load do 73 | # first load a record one way 74 | # on load retry we want to just insure the contents are loaded, but we are still pointing the same instance 75 | @r1 ||= User.find_by_email("mitch@catprint.com").todo_items.first 76 | @r1.title # just so we grab something that is not the id 77 | @r1 78 | end.then do |r1| 79 | ReactiveRecord.load do 80 | # now repeat but get teh record a different way, this will return a different instance 81 | @r2 ||= TodoItem.find_by_title("#{r1.title}") # to make sure there is no magic lets make the title into a new string 82 | @r2.description # lets get the description, when loaded the two record ids will match and will be merged 83 | @r2 84 | end.then do |r2| 85 | async do 86 | expect(r1.description).to eq(r2.description) 87 | expect(r1).to eq(r2) 88 | expect(r1).not_to be(r2) 89 | end 90 | end 91 | end 92 | end 93 | 94 | async "will load a record by indexing a collection" do 95 | ReactiveRecord.load do 96 | User.find_by_email("mitch@catprint.com").todo_items.collect { |todo| todo.description } 97 | end.then do |descriptions| 98 | React::IsomorphicHelpers.load_context 99 | ReactiveRecord.load do 100 | User.find_by_email("mitch@catprint.com").todo_items[1].description 101 | end.then do |description| 102 | async { expect(description).to eq(descriptions[1]) } 103 | end 104 | end 105 | end 106 | 107 | it "will load nested collections correctly" do 108 | ReactiveRecord.load do 109 | User.find_by_email("test1@catprint.com").todo_items.collect do |todo| 110 | todo.comments.collect do |comment| 111 | comment.comment 112 | end 113 | end 114 | end.then do | comments | 115 | expect(comments).to eq([["test 1 todo 1 comment 1", "test 1 todo 1 comment 2"],["test 1 todo 2 comment 1", "test 1 todo 2 comment 2"]]) 116 | end 117 | end 118 | 119 | async "will not fetch model.all when saving a new record to the model" do 120 | (new_record = User.new(email: "test22@catprint.com")).save do 121 | new_record.destroy.then do 122 | async { expect(ReactiveRecord::Base.class_scopes(User)[:all]).to be_nil } 123 | end 124 | end 125 | end 126 | 127 | end 128 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/enum_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | use_case "reading and writting enums" do 4 | 5 | async "can change the enum and read it back" do 6 | React::IsomorphicHelpers.load_context 7 | set_acting_user "super-user" 8 | user = User.find(1) 9 | user.test_enum = :no 10 | user.save.then do 11 | React::IsomorphicHelpers.load_context 12 | ReactiveRecord.load do 13 | User.find(1).test_enum 14 | end.then do |test_enum| 15 | async { expect(test_enum).to eq(:no) } 16 | end 17 | end 18 | end 19 | 20 | async "can set it back" do 21 | React::IsomorphicHelpers.load_context 22 | set_acting_user "super-user" 23 | user = User.find(1) 24 | user.test_enum = :yes 25 | user.save.then do 26 | React::IsomorphicHelpers.load_context 27 | ReactiveRecord.load do 28 | User.find(1).test_enum 29 | end.then do |test_enum| 30 | async { expect(test_enum).to eq(:yes) } 31 | end 32 | end 33 | end 34 | 35 | it "can change it back" do 36 | user = User.find(1) 37 | user.test_enum = :yes 38 | user.save.then do |success| 39 | expect(success).to be_truthy 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/instance_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | #Opal::RSpec::Runner.autorun 3 | class Thing < ActiveRecord::Base 4 | end 5 | 6 | describe "ActiveRecord" do 7 | before(:each) { React::IsomorphicHelpers.load_context } 8 | let(:instance) { Thing.new({attr1: 1, attr2: 2, id: 123}) } 9 | after(:each) { React::API.clear_component_class_cache } 10 | 11 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work 12 | 13 | # describe "a passing dummy test" do 14 | # it "passes" do 15 | # expect(true).to be(true) 16 | # end 17 | # end 18 | 19 | describe "Instance Methods" do 20 | 21 | it "will have the attributes loaded" do 22 | expect(instance.attr1).to eq(1) 23 | end 24 | 25 | it "will not have a primary key if loaded from a hash" do 26 | expect(instance.id).to be(nil) 27 | end 28 | 29 | it "reports being changed if new" do 30 | expect(instance.changed?).to be_truthy 31 | end 32 | 33 | it "reports not being changed if loaded from db" do 34 | expect(Thing.find(123).changed?).to be_falsy 35 | end 36 | 37 | it "reports being changed, if I do change it" do 38 | Thing.find(1234).my_attribute = "new" 39 | expect(Thing.find(1234).changed?).to be_truthy 40 | end 41 | 42 | it "does not think things are destroyed" do 43 | expect(instance).not_to be_destroyed 44 | end 45 | 46 | it "can destroy things" do 47 | instance.destroy 48 | expect(instance).to be_destroyed 49 | end 50 | 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/many_to_many_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "many to many associations" do 4 | 5 | it "it is time to count some comments" do 6 | React::IsomorphicHelpers.load_context 7 | ReactiveRecord.load do 8 | TodoItem.find_by_title("a todo for mitch").comments.count 9 | end.then do |count| 10 | expect(count).to be(1) 11 | end 12 | end 13 | 14 | it "is time to see who made the comment" do 15 | ReactiveRecord.load do 16 | TodoItem.find_by_title("a todo for mitch").comments.first.user.email 17 | end.then do |email| 18 | expect(email).to eq("adamg@catprint.com") 19 | end 20 | end 21 | 22 | it "is time to get it directly through the relationship" do 23 | ReactiveRecord.load do 24 | TodoItem.find_by_title("a todo for mitch").commenters.first.email 25 | end.then do |email| 26 | expect(email).to eq("adamg@catprint.com") 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/non_ar_aggregations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "using non-ar aggregations" do 4 | 5 | it "is time to create a new user and add some data to it" do 6 | React::IsomorphicHelpers.load_context 7 | expect(User.new(first_name: "Data", data: TestData.new("hello", 3)).data.big_string).to eq("hellohellohello") 8 | end 9 | 10 | async "can be saved and restored" do 11 | User.find_by_first_name("Data").save.then do 12 | React::IsomorphicHelpers.load_context 13 | ReactiveRecord.load do 14 | User.find_by_first_name("Data").data 15 | end.then do |data| 16 | async { expect(data.big_string).to eq("hellohellohello") } 17 | end 18 | end 19 | end 20 | 21 | async "is time to change it, and force the save" do 22 | user = User.find_by_first_name("Data") 23 | user.data.string = "goodby" 24 | user.save(force: true).then do 25 | React::IsomorphicHelpers.load_context 26 | ReactiveRecord.load do 27 | User.find_by_first_name("Data").data 28 | end.then do |data| 29 | async { expect(data.big_string).to eq("goodbygoodbygoodby") } 30 | end 31 | end 32 | end 33 | 34 | async "is time to change the value completely and save it (no force needed)" do 35 | user = User.find_by_first_name("Data") 36 | user.data = TestData.new("the end", 1) 37 | user.save.then do 38 | React::IsomorphicHelpers.load_context 39 | ReactiveRecord.load do 40 | User.find_by_first_name("Data").data 41 | end.then do |data| 42 | async { expect(data.big_string).to eq("the end") } 43 | end 44 | end 45 | end 46 | 47 | async "is time to delete the value and see if returns nil after saving" do 48 | user = User.find_by_first_name("Data") 49 | user.data = nil 50 | user.save.then do 51 | React::IsomorphicHelpers.load_context 52 | ReactiveRecord.load do 53 | User.find_by_first_name("Data").data 54 | end.then do |data| 55 | async { expect(data).to be_nil } 56 | end 57 | end 58 | end 59 | 60 | it "is time to delete our user" do 61 | User.find_by_first_name("Data").destroy.then do 62 | expect(User.find_by_first_name("Data")).to be_destroyed 63 | end 64 | end 65 | 66 | it "is time to see to make sure a nil aggregate that has never had a value returns nil" do 67 | ReactiveRecord.load do 68 | User.find_by_email("mitch@catprint.com").data 69 | end.then do |data| 70 | expect(data).to be_nil 71 | end 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/permissions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # for testing convienience we do an odd thing: if acting_user is nil we treat it as a super user 4 | # other wise we can provide an email which will be converted by the application controller into the acting_user 5 | 6 | describe "checking permissions" do 7 | 8 | it "will use the default permissions" do 9 | React::IsomorphicHelpers.load_context 10 | set_acting_user "super-user" 11 | ReactiveRecord.load do 12 | User.find_by_email("todd@catprint.com").id 13 | end.then do |id| 14 | expect(id).not_to be_nil 15 | end 16 | end 17 | 18 | it "will reject a view by the wrong user" do 19 | React::IsomorphicHelpers.load_context 20 | ReactiveRecord.load do |failure| 21 | if failure 22 | failure 23 | else 24 | set_acting_user "todd@catprint.com" 25 | User.find_by_email("mitch@catprint.com").id 26 | end 27 | end.then do |failure| 28 | set_acting_user "super-user" 29 | expect(failure).to include("ReactiveRecord::AccessViolation") 30 | end 31 | end 32 | 33 | it "will honor a correct view permission" do 34 | ReactiveRecord.load do 35 | set_acting_user 'mitch@catprint.com' 36 | User.find_by_first_name("Mitch").last_name 37 | end.then do |last_name| 38 | set_acting_user "super-user" 39 | expect(last_name).to eq("VanDuyn") 40 | end 41 | end 42 | 43 | it "will show data if the user is correct" do 44 | ReactiveRecord.load do 45 | set_acting_user 'mitch@catprint.com' 46 | TodoItem.find_by_title("a todo for mitch").description 47 | end.then do |description| 48 | set_acting_user "super-user" 49 | expect(description).not_to be_nil 50 | end 51 | end 52 | 53 | it "will not show data if the user is not correct" do 54 | React::IsomorphicHelpers.load_context 55 | ReactiveRecord.load do |failure| 56 | if failure 57 | failure 58 | else 59 | set_acting_user 'todd@catprint.com' 60 | TodoItem.find_by_title("a todo for mitch").description 61 | end 62 | end.then do |failure| 63 | set_acting_user "super-user" 64 | expect(failure).to include("ReactiveRecord::AccessViolation") 65 | end 66 | end 67 | 68 | it "is time to make sure mitch is loaded" do 69 | ReactiveRecord.load do 70 | User.find_by_email("mitch@catprint.com").todo_items.collect { |todo| todo.title } 71 | end.then do |todos| 72 | expect(todos).not_to be_nil 73 | end 74 | end 75 | 76 | it "is time to give mitch a new todo" do 77 | mitch = User.find_by_email("mitch@catprint.com") 78 | mitch.todo_items << TodoItem.new({title: "new todo for you"}) 79 | mitch.save.then { |result| expect(result[:success]).to be_truthy } 80 | end 81 | 82 | it "should let mitch update the description" do 83 | new_todo = TodoItem.find_by_title("new todo for you") 84 | new_todo.description = "blah blah blah" 85 | set_acting_user 'mitch@catprint.com' 86 | new_todo.save.then do |result| 87 | expect(result[:success]).to be_truthy 88 | end 89 | end 90 | 91 | it "should not let somebody else update the description" do 92 | new_todo = TodoItem.find_by_title("new todo for you") 93 | new_todo.description = "I can't do this..." 94 | set_acting_user 'todd@catprint.com' 95 | new_todo.save.then do |result| 96 | expect(result[:success]).to be_falsy 97 | end 98 | end 99 | 100 | async "should let users add their own comment (tests create_permitted)" do 101 | React::IsomorphicHelpers.load_context 102 | ReactiveRecord.load do 103 | set_acting_user "super-user" 104 | TodoItem.find_by_title("new todo for you").comments.all 105 | User.find_by_email("todd@catprint.com").id 106 | end.then do 107 | new_todo = TodoItem.find_by_title("new todo for you") 108 | new_todo.comments << Comment.new({comment: "a comment", user: User.find_by_email("todd@catprint.com")}) 109 | set_acting_user 'todd@catprint.com' 110 | new_todo.save.then do |result| 111 | async { expect(result[:success]).to be_truthy } 112 | end 113 | end 114 | end 115 | 116 | async "should not let a user add another user's comment (tests create_permitted)" do 117 | React::IsomorphicHelpers.load_context 118 | ReactiveRecord.load do 119 | set_acting_user "super-user" 120 | TodoItem.find_by_title("new todo for you").comments.count 121 | User.find_by_email("todd@catprint.com").id 122 | end.then do 123 | new_todo = TodoItem.find_by_title("new todo for you") 124 | new_todo.comments << Comment.new({comment: "a comment", user: User.find_by_email("todd@catprint.com")}) 125 | set_acting_user 'mitch@catprint.com' 126 | new_todo.save.then do |result| 127 | async { expect(result[:success]).to be_falsy } 128 | end 129 | end 130 | end 131 | 132 | it "is time to make sure things really did work" do 133 | React::IsomorphicHelpers.load_context 134 | ReactiveRecord.load do 135 | set_acting_user "super-user" 136 | todo = TodoItem.find_by_title("new todo for you") 137 | [todo.user.first_name, todo.description, todo.comments.count, todo.comments.first.comment, todo.comments.first.user.email] 138 | end.then do |results| 139 | expect(results).to eq(["Mitch", "blah blah blah", 1, "a comment", "todd@catprint.com"]) 140 | end 141 | end 142 | 143 | it "is time to delete the comment - lets see if mitch can delete it" do 144 | comment = TodoItem.find_by_title("new todo for you").comments.first 145 | set_acting_user 'mitch@catprint.com' 146 | comment.destroy.then { |result| expect(result[:success]).to be_falsy } 147 | end 148 | 149 | async "is time to delete the comment - lets do it without the promise" do 150 | React::IsomorphicHelpers.load_context 151 | comment = TodoItem.find_by_title("new todo for you").comments.first 152 | set_acting_user 'mitch@catprint.com' 153 | comment.destroy { |result, message| async { expect(result).to be_falsy; expect(message).to be_present } } 154 | end 155 | 156 | it "is time to really delete it" do 157 | React::IsomorphicHelpers.load_context 158 | set_acting_user 'todd@catprint.com' 159 | comment = Comment.find_by_comment("a comment") 160 | comment.destroy.then { |result| expect(result[:success]).to be_truthy } 161 | end 162 | 163 | it "is time to delete the todo" do 164 | set_acting_user "super-user" 165 | new_todo = TodoItem.find_by_title("new todo for you") 166 | new_todo.destroy.then { |result| expect(result[:success]).to be_truthy } 167 | end 168 | 169 | end 170 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/prerendering_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'components/test' 3 | 4 | describe "prerendering" do 5 | 6 | it "will not return an id before preloading" do 7 | React::IsomorphicHelpers.load_context 8 | expect(User.find_by_email("mitch@catprint.com").id).not_to eq(1) 9 | end 10 | 11 | async "preloaded the records" do 12 | `window.ClientSidePrerenderDataInterface.ReactiveRecordInitialData = undefined` rescue nil 13 | container = Element[Document.body].append('
').children.last 14 | complete = lambda do 15 | React::IsomorphicHelpers.load_context 16 | async do 17 | mitch = User.find_by_email("mitch@catprint.com") 18 | expect(mitch.id).to eq(1) 19 | expect(mitch.first_name).to eq("Mitch") 20 | expect(mitch.todo_items.first.title).to eq("a todo for mitch") 21 | expect(mitch.address.zip).to eq("14617") 22 | expect(mitch.todo_items.find_string("mitch").first.title).to eq("a todo for mitch") 23 | expect(mitch.todo_items.first.commenters.first.email).to eq("adamg@catprint.com") 24 | expect(mitch.expensive_math(13)).to eq(169) 25 | expect(mitch.detailed_name).to eq("M. VanDuyn - mitch@catprint.com (2 todos)") 26 | # clear out everything before moving on otherwise the initial data screws up the next test 27 | `delete window.ReactiveRecordInitialData` 28 | React::IsomorphicHelpers.load_context 29 | end 30 | end 31 | `container.load('/test', complete)` 32 | end 33 | 34 | async "does not preload everything" do 35 | `window.ClientSidePrerenderDataInterface.ReactiveRecordInitialData = undefined` rescue nil 36 | container = Element[Document.body].append('
').children.last 37 | complete = lambda do 38 | React::IsomorphicHelpers.load_context 39 | async do 40 | expect(User.find_by_email("mitch@catprint.com").last_name.to_s).to eq("") 41 | # clear out everything before moving on otherwise there will be a pending load that screw up the next test 42 | `delete window.ReactiveRecordInitialData` 43 | React::IsomorphicHelpers.load_context 44 | end 45 | end 46 | `container.load('/test', complete)` 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/reactive_record_load_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "ReactiveRecord.load" do 4 | 5 | it "will not find a non-existing record" do 6 | React::IsomorphicHelpers.load_context 7 | ReactiveRecord.load do 8 | User.find_by_first_name("Jon").id 9 | end.then do |id| 10 | expect(id).to be_nil 11 | end 12 | end 13 | 14 | it "will find an existing record" do 15 | React::IsomorphicHelpers.load_context 16 | ReactiveRecord.load do 17 | User.find_by_email("todd@catprint.com").id 18 | end.then do |id| 19 | expect(id).not_to be_nil 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/rendering_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | #require 'user' 3 | #require 'todo_item' 4 | #require 'address' 5 | 6 | describe "integration with react" do 7 | 8 | before(:each) { React::IsomorphicHelpers.load_context } 9 | 10 | it "find by two methods will not give the same object until loaded" do 11 | r1 = User.find_by_email("mitch@catprint.com") 12 | r2 = User.find_by_first_name("Mitch") 13 | expect(r1).not_to eq(r2) 14 | end 15 | 16 | rendering("find by two methods gives same object once loaded") do 17 | r1 = User.find_by_email("mitch@catprint.com") 18 | r2 = User.find_by_first_name("Mitch") 19 | r1.id 20 | r2.id 21 | if r1 == r2 22 | "SAME OBJECT" 23 | else 24 | "NOT YET" 25 | end 26 | end.should_generate do 27 | html == "SAME OBJECT" 28 | end 29 | 30 | it "will find two different attributes will not be equal before loading" do 31 | r1 = User.find_by_email("mitch@catprint.com") 32 | expect(r1.first_name).not_to eq(r1.last_name) 33 | end 34 | 35 | it "will find the same attributes to be equal before loading" do 36 | r1 = User.find_by_email("mitch@catprint.com") 37 | expect(r1.first_name).to eq(r1.first_name) 38 | end 39 | 40 | rendering("find by two methods gives same attributes once loaded") do 41 | r1 = User.find_by_email("mitch@catprint.com") 42 | r2 = User.find_by_first_name("Mitch") 43 | if r1.first_name == r2.first_name 44 | "SAME VALUE" 45 | else 46 | "NOT YET" 47 | end 48 | end.should_generate do 49 | html == "SAME VALUE" 50 | end 51 | 52 | it "will know that an attribute is loading" do 53 | r1 = User.find_by_email("mitch@catprint.com") 54 | expect(r1.first_name).to be_loading 55 | end 56 | 57 | rendering("an attribute will eventually set it not loading") do 58 | User.find_by_email("mitch@catprint.com").first_name.loading? ? "LOADING" : "LOADED" 59 | end.should_generate do 60 | html == "LOADED" 61 | end 62 | 63 | it "will know that an attribute is not loaded" do 64 | r1 = User.find_by_email("mitch@catprint.com") 65 | expect(r1.first_name).not_to be_loaded 66 | end 67 | 68 | rendering("an attribute will eventually set it loaded") do 69 | User.find_by_email("mitch@catprint.com").first_name.loaded? ? "LOADED" : "LOADING" 70 | end.should_generate do 71 | html == "LOADED" 72 | end 73 | 74 | it "present? returns true for a non-nil value" do 75 | expect("foo").to be_present 76 | end 77 | 78 | it "present? returns false for nil" do 79 | expect(false).not_to be_present 80 | end 81 | 82 | it "will consider a unloaded attribute not to be present" do 83 | r1 = User.find_by_email("mitch@catprint.com") 84 | expect(r1.first_name).not_to be_present 85 | end 86 | 87 | rendering("a non-nil attribute will make it present") do 88 | User.find_by_email("mitch@catprint.com").first_name.present? ? "PRESENT" : "" 89 | end.should_generate do 90 | html == "PRESENT" 91 | end 92 | 93 | rendering("a simple find_by query") do 94 | User.find_by_email("mitch@catprint.com").email 95 | end.should_immediately_generate do 96 | html == "mitch@catprint.com" 97 | end 98 | 99 | rendering("an attribute from the server") do 100 | User.find_by_email("mitch@catprint.com").first_name 101 | end.should_generate do 102 | html == "Mitch" 103 | end 104 | 105 | rendering("a has_many association") do 106 | User.find_by_email("mitch@catprint.com").todo_items.collect do |todo| 107 | todo.title 108 | end.join(", ") 109 | end.should_generate do 110 | html == "a todo for mitch, another todo for mitch" 111 | end 112 | 113 | rendering("only as many times as needed") do 114 | times_up = React::State.get_state(self, "times_up") 115 | @timer ||= after(0.5) { React::State.set_state(self, "times_up", "DONE")} 116 | @count ||= 0 117 | @count += 1 118 | "#{times_up} #{@count.to_s} " + User.find_by_email("mitch@catprint.com").todo_items.collect do |todo| 119 | todo.title 120 | end.join(", ") 121 | end.should_generate do 122 | puts "trying again: #{html}" 123 | html == "DONE 3 a todo for mitch, another todo for mitch" 124 | end 125 | 126 | rendering("a belongs_to association from id") do 127 | TodoItem.find(1).user.email 128 | end.should_generate do 129 | html == "mitch@catprint.com" 130 | end 131 | 132 | rendering("a belongs_to association from an attribute") do 133 | User.find_by_email("mitch@catprint.com").todo_items.first.user.email 134 | end.should_generate do 135 | html == "mitch@catprint.com" 136 | end 137 | 138 | rendering("an aggregation") do 139 | User.find_by_email("mitch@catprint.com").address.city 140 | end.should_generate do 141 | html == "Rochester" 142 | end 143 | 144 | rendering("a record that is updated multiple times") do 145 | unless @record 146 | @record = User.new 147 | @record.attributes[:all_done] = false 148 | @record.attributes[:test_done] = false 149 | @record.attributes[:counter] = 0 150 | end 151 | puts "rendering #{@record} #{@record.attributes[:counter]}" 152 | after(0.1) do 153 | puts "update counter timer expired, @record.test_done = #{!!@record.test_done}" 154 | @record.counter = @record.counter + 1 unless @record.test_done 155 | end 156 | puts "record.changed? #{!!@record.changed?}" 157 | after(2) do 158 | puts "all done timer expired test should get done now!" 159 | @record.all_done = true 160 | end unless @record.changed? 161 | if @record.all_done 162 | @record.all_done = nil 163 | @record.test_done = true 164 | "#{@record.counter}" 165 | else 166 | "not done yet... #{@record.changed?}, #{@record.attributes[:counter]}" 167 | end 168 | end.should_generate do 169 | puts "html = #{html}" 170 | html == "2" 171 | end 172 | 173 | rendering("changing an aggregate is noticed by the parent") do 174 | @user ||= User.find_by_email("mitch@catprint.com") 175 | after(0.1) do 176 | @user.address.city = "Timbuktoo" 177 | end 178 | if @user.changed? 179 | "#{@user.address.city}" 180 | end 181 | end.should_generate do 182 | html == "Timbuktoo" 183 | end 184 | 185 | rendering("a server side value dynamically changed before first fetch from server") do 186 | puts "rendering" 187 | @update ||= after(0.001) do 188 | puts "async update" 189 | mitch = User.find_by_email("mitch@catprint.com") 190 | mitch.first_name = "Robert" 191 | mitch.detailed_name! 192 | puts "updated" 193 | end 194 | User.find_by_email("mitch@catprint.com").detailed_name 195 | end.should_generate do 196 | puts "html = #{html}" 197 | html == "R. VanDuyn - mitch@catprint.com (2 todos)" 198 | end 199 | 200 | rendering("a server side value dynamically changed after first fetch from server") do 201 | puts "rendering" 202 | @update ||= after(1) do 203 | puts "async update" 204 | mitch = User.find_by_email("mitch@catprint.com") 205 | mitch.first_name = "Robert" 206 | mitch.detailed_name! 207 | puts "updated" 208 | end 209 | User.find_by_email("mitch@catprint.com").detailed_name 210 | end.should_generate do 211 | puts "html = #{html}" 212 | html == "R. VanDuyn - mitch@catprint.com (2 todos)" 213 | end 214 | 215 | end 216 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/revert_record_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | #require 'user' 3 | #require 'todo_item' 4 | 5 | describe "reverting records" do 6 | 7 | it "finds that the user Adam has not changed yet" do 8 | React::IsomorphicHelpers.load_context 9 | ReactiveRecord.load do 10 | User.find_by_first_name("Adam") 11 | end.then do |user| 12 | expect(user).not_to be_changed 13 | end 14 | end 15 | 16 | it "creates a new todo which should be changed (because its new)" do 17 | new_todo = TodoItem.new({title: "Adam is not getting this todo"}) 18 | expect(new_todo).to be_changed 19 | end 20 | 21 | it "adds the todo to adam's todos and expects adam to change" do 22 | adam = User.find_by_first_name("Adam") 23 | adam.todo_items << TodoItem.find_by_title("Adam is not getting this todo") 24 | expect(adam).to be_changed 25 | end 26 | 27 | it "will show that the new todo is still changed" do 28 | expect(TodoItem.find_by_title("Adam is not getting this todo")).to be_changed 29 | end 30 | 31 | it "can be reverted and the todo will not be changed" do 32 | todo = TodoItem.find_by_title("Adam is not getting this todo") 33 | todo.revert 34 | expect(todo).not_to be_changed 35 | end 36 | 37 | it "will not have changed adam" do 38 | adam = User.find_by_first_name("Adam") 39 | expect(User.find_by_first_name("Adam")).not_to be_changed 40 | end 41 | 42 | it "is time to test going the other way, lets give adam a todo again" do 43 | new_todo = TodoItem.new({title: "Adam is still not getting this todo"}) 44 | adam = User.find_by_first_name("Adam") 45 | adam.todo_items << new_todo 46 | expect(adam).to be_changed 47 | end 48 | 49 | it "an be revert" do 50 | adam = User.find_by_first_name("Adam") 51 | adam.revert 52 | expect(adam).not_to be_changed 53 | end 54 | 55 | it "finds the todo is still changed" do 56 | expect(TodoItem.find_by_title("Adam is still not getting this todo")).to be_changed 57 | end 58 | 59 | async "can change an attribute, revert, and make sure nothing else changes" do 60 | ReactiveRecord.load do 61 | User.find_by_email("mitch@catprint.com").last_name 62 | User.find_by_email("mitch@catprint.com").todo_items.all.count 63 | end.then do |original_todo_count| 64 | puts "current todo count for mitch: #{original_todo_count}" 65 | mitch = User.find_by_email("mitch@catprint.com") 66 | original_last_name = mitch.last_name 67 | mitch.last_name = "xxxx" 68 | mitch.save do 69 | mitch.revert 70 | new_items_count = mitch.todo_items.all.count 71 | mitch.last_name = original_last_name 72 | mitch.save.then do 73 | async { expect(new_items_count).to eq(original_todo_count) } 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/save_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "simple record update and save" do 4 | 5 | it "can find an existing model" do 6 | React::IsomorphicHelpers.load_context 7 | ReactiveRecord.load do 8 | User.find_by_email("mitch@catprint.com").first_name 9 | end.then do |first_name| 10 | expect(first_name).to be("Mitch") 11 | end 12 | end 13 | 14 | it "doesn't find the model changed" do 15 | expect(User.find_by_email("mitch@catprint.com")).not_to be_changed 16 | end 17 | 18 | it "the model is not new" do 19 | expect(User.find_by_email("mitch@catprint.com")).not_to be_new 20 | end 21 | 22 | it "the model is not saving" do 23 | expect(User.find_by_email("mitch@catprint.com")).not_to be_saving 24 | end 25 | 26 | it "an attribute can be changed" do 27 | mitch = User.find_by_email("mitch@catprint.com") 28 | mitch.first_name = "Mitchell" 29 | expect(mitch.first_name).to eq("Mitchell") 30 | end 31 | 32 | it "and the attribute will be marked as changed"do 33 | expect(User.find_by_email("mitch@catprint.com")).to be_changed 34 | end 35 | 36 | it "saving? is true while the model is being saved" do 37 | mitch = User.find_by_email("mitch@catprint.com") 38 | mitch.save.then {}.tap { expect(mitch).to be_saving } 39 | end 40 | 41 | it "after saving changed? will be false" do 42 | expect(User.find_by_email("mitch@catprint.com")).not_to be_changed 43 | end 44 | 45 | it "after saving saving? will be false" do 46 | expect(User.find_by_email("mitch@catprint.com")).not_to be_saving 47 | end 48 | 49 | it "the data has been persisted to the database" do 50 | React::IsomorphicHelpers.load_context 51 | ReactiveRecord.load do 52 | User.find_by_email("mitch@catprint.com").first_name 53 | end.then do |first_name| 54 | expect(first_name).to eq("Mitchell") 55 | end 56 | end 57 | 58 | it "after saving within the block saving? will be false" do 59 | mitchell = User.find_by_email("mitch@catprint.com") 60 | mitchell.first_name = "Mitch" 61 | mitchell.save.then do 62 | expect(mitchell).not_to be_saving 63 | end 64 | end 65 | 66 | async "the save block receives the correct block parameters" do 67 | mitch = User.find_by_email("mitch@catprint.com") 68 | mitch.first_name = "Mitchell" 69 | mitch.save do | success, message, models | 70 | async do 71 | expect(success).to be_truthy 72 | expect(message).to be_nil 73 | expect(models).to eq([mitch]) 74 | expect(mitch.errors).to be_empty 75 | end 76 | end 77 | end 78 | 79 | it "the save promise receives the response hash" do 80 | mitch = User.find_by_email("mitch@catprint.com") 81 | mitch.first_name = "Mitch" 82 | mitch.save.then do | response | 83 | expect(response[:success]).to be_truthy 84 | expect(response[:message]).to be_nil 85 | expect(response[:models]).to eq([mitch]) 86 | end 87 | end 88 | 89 | async "the save will fail if validation fails" do 90 | mitch = User.find_by_email("mitch@catprint.com") 91 | mitch.email = "mitch at catprint dot com" 92 | mitch.save do |success, message, models| 93 | async do 94 | expect(success).to be_falsy 95 | expect(message).to be_present 96 | expect(models).to eq([mitch]) 97 | end 98 | end 99 | end 100 | 101 | it "validation errors are put in the errors object" do 102 | mitch = User.find_by_email("mitch@catprint.com") 103 | mitch.email = "mitch at catprint dot com" 104 | mitch.save.then do |success, message, models| 105 | expect(mitch.errors[:email]).to eq(["is invalid"]) 106 | end 107 | end 108 | 109 | it "within the save block saving? is false if validation fails" do 110 | mitch = User.find_by_email("mitch@catprint.com") 111 | mitch.email = "mitch at catprint dot com" 112 | mitch.save.then do 113 | expect(mitch).not_to be_saving 114 | end 115 | end 116 | 117 | it "if validation fails changed? is still true" do 118 | mitch = User.find_by_email("mitch@catprint.com") 119 | mitch.email = "mitch at catprint dot com" 120 | mitch.save.then do 121 | expect(mitch).to be_changed 122 | end 123 | end 124 | 125 | end 126 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/scope_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "can scope models" do 4 | 5 | it "scopes todos by string" do 6 | React::IsomorphicHelpers.load_context 7 | ReactiveRecord.load do 8 | User.find_by_email("mitch@catprint.com").todo_items.find_string("mitch").first.title 9 | end.then do |title| 10 | expect(title).to be("a todo for mitch") 11 | end 12 | end 13 | 14 | it "can apply multiple simple scopes" do 15 | React::IsomorphicHelpers.load_context 16 | ReactiveRecord.load do 17 | User.find_by_email("mitch@catprint.com").todo_items.active.important.first.title 18 | end.then do |title| 19 | expect(title).to be("another todo for mitch") 20 | end 21 | end 22 | 23 | it "can apply multiple scopes" do 24 | ReactiveRecord.load do 25 | User.find_by_email("mitch@catprint.com").todo_items.find_string("mitch").find_string("another").count 26 | end.then do |count| 27 | expect(count).to be(1) 28 | end 29 | end 30 | 31 | it "can apply scopes to model" do 32 | ReactiveRecord.load do 33 | TodoItem.find_string("mitch").first.title 34 | end.then do |title| 35 | expect(title).to be("a todo for mitch") 36 | end 37 | end 38 | 39 | it "works for an empty set" do 40 | ReactiveRecord.load do 41 | User.find_by_email("adamg@catprint.com").todo_items.find_string("mitch").find_string("another").collect do |item| 42 | item.title 43 | end 44 | end.then do |result| 45 | expect(result).to eq([]) 46 | end 47 | end 48 | 49 | 50 | it "works for an empty set even if other items are retrieved" do 51 | React::IsomorphicHelpers.load_context 52 | ReactiveRecord.load do 53 | user = User.find(3) 54 | user.todo_items.find_string("mitch").find_string("another").collect do |item| 55 | item.title 56 | end 57 | end.then do |result| 58 | expect(result).to eq([]) 59 | end 60 | end 61 | 62 | it "reports that a collection is loading" do 63 | React::IsomorphicHelpers.load_context 64 | expect(User.find_by_email("mitch@catprint.com").todo_items).to be_loading 65 | end 66 | 67 | it "reports that a collection has loaded" do 68 | ReactiveRecord.load do 69 | User.find_by_email("mitch@catprint.com").todo_items.first.title 70 | end.then do |title| 71 | expect(User.find_by_email("mitch@catprint.com").todo_items).to be_loaded 72 | end 73 | end 74 | 75 | it "can return the count of a collection without loading the collection" do 76 | React::IsomorphicHelpers.load_context 77 | ReactiveRecord.load do 78 | User.find_by_email("mitch@catprint.com").todo_items.count 79 | end.then do |count| 80 | expect(count).to be(2) 81 | expect(User.find_by_email("mitch@catprint.com").todo_items).to be_loading 82 | end 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/update_aggregations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "updating aggregations" do 4 | 5 | before(:all) do 6 | React::IsomorphicHelpers.load_context 7 | User.new({first_name: "Jon", last_name: "Weaver"}) 8 | end 9 | 10 | it "a hash can be assigned to initialize an aggregate" do 11 | expect(User.new(address: Address.new(zip:12345)).address.zip).to eq(12345) 12 | end 13 | 14 | it "a new model will have a blank aggregate" do 15 | expect(User.find_by_first_name("Jon").address.attributes[:zip]).to be_blank 16 | end 17 | 18 | it "an aggregate can be updated through the parent model" do 19 | User.find_by_first_name("Jon").address.zip = "14609" 20 | expect(User.find_by_first_name("Jon").address.zip).to eq("14609") 21 | end 22 | 23 | async "saving a model, saves the aggregate values" do 24 | User.find_by_first_name("Jon").save.then do 25 | React::IsomorphicHelpers.load_context 26 | ReactiveRecord.load do 27 | User.find_by_first_name("Jon").address.zip 28 | end.then do |zip| 29 | async { expect(zip).to eq("14609") } 30 | end 31 | end 32 | end 33 | 34 | it "an aggregate can be assigned" do 35 | user = User.find_by_first_name("Jon") 36 | user.address = Address.new({zip: "14622", city: "Rochester"}) 37 | expect([user.address.zip, user.address.city]).to eq(["14622", "Rochester"]) 38 | end 39 | 40 | async "and saving the model will save the address" do 41 | User.find_by_first_name("Jon").save.then do 42 | React::IsomorphicHelpers.load_context 43 | ReactiveRecord.load do 44 | [User.find_by_first_name("Jon").address.zip, User.find_by_first_name("Jon").address.city] 45 | end.then do |zip_and_city| 46 | async { expect(zip_and_city).to eq(["14622", "Rochester"]) } 47 | end 48 | end 49 | end 50 | 51 | it "two aggregates of the same type don't not get mixed up" do 52 | ReactiveRecord.load do 53 | User.find_by_first_name("Jon").address2.zip 54 | end.then do |zip| 55 | expect(zip).to be_nil 56 | end 57 | end 58 | 59 | async "can assign a model to an aggregate attribute" do 60 | address = Address.new({zip: "14622", city: "Rochester"}) 61 | address.save do 62 | user = User.new 63 | user.address = address 64 | ReactiveRecord.load do 65 | user.verify_zip 66 | end.then do |value| 67 | async { expect(value).to eq("14622") } 68 | end 69 | end 70 | end 71 | 72 | after(:all) do 73 | Promise.when(Address.all.last.destroy, User.find_by_first_name("Jon").destroy) 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/update_associations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "updating associations" do 4 | 5 | before(:all) do 6 | React::IsomorphicHelpers.load_context 7 | User.new({first_name: "Jon", last_name: "Weaver"}) 8 | end 9 | 10 | it "a new model will have empty has_many assocation" do 11 | jon = User.find_by_first_name("Jon") 12 | expect(jon.todo_items).to be_empty 13 | end 14 | 15 | it "an item can be added to a has_many association" do 16 | jon = User.find_by_first_name("Jon") 17 | result = (jon.todo_items << (item = TodoItem.new({title: "Jon's first todo!"}))) 18 | expect(result).to be(jon.todo_items) 19 | expect(jon.todo_items.count).to be(1) 20 | end 21 | 22 | async "it will persist the new has_many association" do 23 | User.find_by_first_name("Jon").save do 24 | React::IsomorphicHelpers.load_context 25 | ReactiveRecord.load do 26 | User.find_by_first_name("Jon").todo_items.count 27 | end.then do | count | 28 | async { expect(count).to be(1) } 29 | end 30 | end 31 | end 32 | 33 | it "and will reconstruct the association and values on reloading" do 34 | ReactiveRecord.load do 35 | User.find_by_first_name("Jon").todo_items.collect { | todo | todo.title } 36 | end.then do | titles | 37 | expect(titles).to eq(["Jon's first todo!"]) 38 | end 39 | end 40 | 41 | it "the inverse belongs_to association will be set" do 42 | todo = TodoItem.find_by_title("Jon's first todo!") 43 | expect(todo.user.first_name).to eq("Jon") 44 | end 45 | 46 | it "a model can be moved to a new owner, and will be removed from the old owner" do 47 | TodoItem.find_by_title("Jon's first todo!").user = User.new({first_name: "Jan", last_name: "VanDuyn"}) 48 | expect(User.find_by_first_name("Jon").todo_items).to be_empty 49 | end 50 | 51 | it "and will belong to the new owner" do 52 | expect(User.find_by_first_name("Jan").todo_items.all == [TodoItem.find_by_title("Jon's first todo!")]).to be_truthy 53 | end 54 | 55 | async "and can be saved and it will remember its new owner" do 56 | TodoItem.find_by_title("Jon's first todo!").save do 57 | React::IsomorphicHelpers.load_context 58 | ReactiveRecord.load do 59 | TodoItem.find_by_title("Jon's first todo!").user.first_name 60 | end.then do | first_name | 61 | async { expect(first_name).to be("Jan") } 62 | end 63 | end 64 | end 65 | 66 | it "and after saving will have been removed from original owners association" do 67 | ReactiveRecord.load do 68 | User.find_by_first_name("Jon").todo_items.all 69 | end.then do | todos | 70 | expect(todos).to be_empty 71 | end 72 | end 73 | 74 | it "a belongs to association can be set to nil and the model saved" do 75 | todo = TodoItem.find_by_title("Jon's first todo!") 76 | todo.user = nil 77 | todo.save.then do | response | 78 | expect(response[:success]).to be_truthy 79 | end 80 | end 81 | 82 | it "and will not belong to the previous owner anymore" do 83 | React::IsomorphicHelpers.load_context 84 | ReactiveRecord.load do 85 | TodoItem.find_by_title("Jon's first todo!").user # load the todo in prep for the next test 86 | User.find_by_first_name("Jan").todo_items.all.count 87 | end.then do |count| 88 | expect(count).to be(0) 89 | end 90 | end 91 | 92 | it "but can be reassigned to the previous owner" do 93 | todo = TodoItem.find_by_title("Jon's first todo!") 94 | todo.user = User.find_by_first_name("Jan") 95 | todo.save.then do | response | 96 | expect(response[:success]).to be_truthy 97 | end 98 | end 99 | 100 | it "and a model in a belongs_to relationship can be deleted" do 101 | User.find_by_first_name("Jan").todo_items.first.destroy.then do 102 | expect(User.find_by_first_name("Jan").todo_items).to be_empty 103 | end 104 | end 105 | 106 | it "and it won't exist" do 107 | React::IsomorphicHelpers.load_context 108 | ReactiveRecord.load do 109 | TodoItem.find_by_title("Jon's first todo!").id 110 | end.then do | id | 111 | expect(id).to be_nil 112 | end 113 | end 114 | 115 | it "an item in a belongs_to relationship can be created without belonging to anybody" do 116 | nobodys_business = TodoItem.new({title: "round to it"}) 117 | nobodys_business.save.then do |saved| 118 | expect(saved).to be_truthy 119 | end 120 | end 121 | 122 | it "and can be reloaded" do 123 | React::IsomorphicHelpers.load_context 124 | ReactiveRecord.load do 125 | TodoItem.find_by_title("round to it").id 126 | end.then do |id| 127 | expect(id).not_to be_nil 128 | end 129 | end 130 | 131 | it "and can be deleted" do 132 | TodoItem.find_by_title("round to it").destroy.then do 133 | expect(TodoItem.find_by_title("round to it")).to be_destroyed 134 | end 135 | end 136 | 137 | after(:all) do 138 | Promise.when(User.find_by_first_name("Jan").destroy, User.find_by_first_name("Jon").destroy) 139 | end 140 | 141 | 142 | end 143 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/update_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "creating and updating a record" do 4 | 5 | before(:all) do 6 | React::IsomorphicHelpers.load_context 7 | end 8 | 9 | it "can create a new record" do 10 | jon = User.new({first_name: "Jon", last_name: "Weaver"}) 11 | expect(jon.attributes).to eq({first_name: "Jon", last_name: "Weaver"}) 12 | end 13 | 14 | it "after creating it will have no id" do 15 | jon = User.find_by_first_name("Jon") 16 | expect(jon.id).to be_nil 17 | end 18 | 19 | it "after creating it will be new" do 20 | jon = User.find_by_first_name("Jon") 21 | expect(jon).to be_new 22 | end 23 | 24 | it "after creating it will be changed" do 25 | jon = User.find_by_first_name("Jon") 26 | expect(jon.changed?).to be_truthy 27 | end 28 | 29 | it "it can calculate server side attributes before saving" do 30 | ReactiveRecord.load do 31 | User.find_by_first_name("Jon").detailed_name 32 | end.then do |name| 33 | expect(name).to eq("J. Weaver") 34 | end 35 | end 36 | 37 | it "can be saved and will have an id" do 38 | jon = User.find_by_first_name("Jon") 39 | jon.save.then { expect(jon.id).not_to be_nil } 40 | end 41 | 42 | it "can be reloaded" do 43 | React::IsomorphicHelpers.load_context 44 | ReactiveRecord.load do 45 | User.find_by_first_name("Jon").last_name 46 | end.then do |last_name| 47 | expect(last_name).to be("Weaver") 48 | end 49 | end 50 | 51 | it "will still have an id" do 52 | jon = User.find_by_first_name("Jon") 53 | expect(jon.id).not_to be_nil 54 | end 55 | 56 | it "can be updated and it will get new server side values before saving" do 57 | jon = User.find_by_last_name("Weaver") 58 | jon.email = "jonny@catprint.com" 59 | ReactiveRecord.load do 60 | jon.detailed_name 61 | end.then do |detailed_name| 62 | expect(detailed_name).to eq("J. Weaver - jonny@catprint.com") 63 | end 64 | end 65 | 66 | it "can be updated but it won't see the new server side values" do 67 | jon = User.find_by_last_name("Weaver") 68 | jon.email = "jon@catprint.com" 69 | ReactiveRecord.load do 70 | jon.detailed_name 71 | end.then do |detailed_name| 72 | expect(detailed_name).to eq("J. Weaver - jonny@catprint.com") 73 | end 74 | end 75 | 76 | it "but the bang method forces a refresh" do 77 | jon = User.find_by_last_name("Weaver") 78 | ReactiveRecord.load do 79 | jon.detailed_name! unless jon.detailed_name == "J. Weaver - jon@catprint.com" 80 | jon.detailed_name 81 | end.then do |detailed_name| 82 | expect(detailed_name).to eq("J. Weaver - jon@catprint.com") 83 | end 84 | end 85 | 86 | async "can be saved and will remember the new values" do 87 | jon = User.find_by_last_name("Weaver") 88 | jon.email = "jon@catprint.com" 89 | jon.save.then do 90 | React::IsomorphicHelpers.load_context 91 | ReactiveRecord.load do 92 | User.find_by_last_name("Weaver").email 93 | end.then do |email| 94 | async { expect(email).to be("jon@catprint.com") } 95 | end 96 | end 97 | end 98 | 99 | it "can be deleted" do 100 | jon = User.find_by_last_name("Weaver") 101 | jon.destroy.then { expect(jon.id).to be_nil } 102 | end 103 | 104 | it "does not exist in the database" do 105 | React::IsomorphicHelpers.load_context 106 | ReactiveRecord.load do 107 | User.find_by_first_name("Jon").id 108 | end.then do |id| 109 | expect(id).to be_nil 110 | end 111 | end 112 | 113 | async "it can have a one way writable attribute (might be used for a password - see the user model)" do 114 | jon = User.new({name: "Jon Weaver"}) 115 | jon.save.then do 116 | React::IsomorphicHelpers.load_context 117 | ReactiveRecord.load do 118 | User.find_by_last_name("Weaver").first_name 119 | end.then do |first_name| 120 | async { expect(first_name).to be("Jon") } 121 | end 122 | end 123 | end 124 | 125 | after(:all) do 126 | User.find_by_last_name("Weaver").destroy 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/update_scopes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "updating scopes" do 4 | 5 | # this spec needs some massive cleanup... the rendering tests continue to run... that needs to be fixed 6 | 7 | # the tests depend on each other. 8 | 9 | # there are no test for nested scopes like User.todos.active for example which will certainly fail 10 | 11 | rendering("saving a new record will update .all and cause a rerender") do 12 | unless @starting_count 13 | TodoItem.all.last.title 14 | unless TodoItem.all.count == 1 15 | @starting_count = TodoItem.all.count 16 | 17 | after(0.1) do 18 | TodoItem.new(title: "play it again sam").save 19 | end 20 | end 21 | end 22 | (TodoItem.all.count - (@starting_count || 100)).to_s 23 | end.should_generate do 24 | html == "1" 25 | end 26 | 27 | rendering("adding a new matching record will add the record to a scope using abbreviated to_sync macro") do 28 | unless @starting_count 29 | unless TodoItem.important.first.description.loading? 30 | @starting_count = TodoItem.important.count 31 | after(0.1) do 32 | td = TodoItem.new(description: "another big mitch todo XXX") 33 | td.save do 34 | puts "after save, pushing #{TodoItem.important} << #{td}" 35 | #TodoItem.important << td 36 | end 37 | end 38 | end 39 | end 40 | (TodoItem.important.count - (@starting_count || 100)).to_s 41 | end.should_generate do 42 | html == "1" 43 | end 44 | 45 | rendering("adding a new matching record will add the record to a scope using abbreviated to_sync macro") do 46 | unless @starting_count 47 | unless TodoItem.important.first.description.loading? 48 | @starting_count = TodoItem.important.count 49 | after(0.1) do 50 | TodoItem.new(description: "another big mitch todo XXX").save 51 | end 52 | end 53 | end 54 | (TodoItem.important.count - (@starting_count || 100)).to_s 55 | end.should_generate do 56 | html == "1" 57 | end 58 | 59 | rendering("adding a new matching record will add the record to a scope using full to_sync macro") do 60 | unless @starting_count 61 | unless TodoItem.active.first.title.loading? 62 | @starting_count = TodoItem.active.count 63 | after(0.1) do 64 | TodoItem.new(title: "another big mitch todo XXX").save 65 | end 66 | end 67 | end 68 | (TodoItem.active.count - (@starting_count || 100)).to_s 69 | end.should_generate do 70 | html == "1" 71 | end 72 | 73 | rendering("destroying records will cause a re-render") do 74 | unless @starting_count 75 | TodoItem.all.last.title 76 | unless TodoItem.all.count == 1 77 | @starting_count = TodoItem.all.count 78 | after(0.1) do 79 | TodoItem.all.last.destroy do 80 | TodoItem.all.last.destroy do 81 | TodoItem.all.last.destroy do 82 | TodoItem.all.last.destroy 83 | end 84 | end 85 | end 86 | end 87 | end 88 | end 89 | (TodoItem.all.count - (@starting_count || 100)).to_s 90 | end.should_generate do 91 | html == "-3" 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/active_record/virtual_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "virtual attributes" do 4 | 5 | it "can call a virtual method on the server" do 6 | React::IsomorphicHelpers.load_context 7 | ReactiveRecord.load do 8 | User.find(1).expensive_math(5) 9 | end.then { |virtual_answer| expect(virtual_answer).to eq(25) } 10 | end 11 | 12 | it "can call a virtual method on a new model on the server" do 13 | React::IsomorphicHelpers.load_context 14 | new_user = User.new 15 | ReactiveRecord.load do 16 | new_user.expensive_math(4) 17 | end.then { |virtual_answer| expect(virtual_answer).to eq(16) } 18 | end 19 | 20 | it "can call a simple virtual method on a new model on the server" do 21 | React::IsomorphicHelpers.load_context 22 | new_user = User.new 23 | ReactiveRecord.load do 24 | new_user.detailed_name 25 | end.then { |virtual_answer| expect(virtual_answer).to eq("") } 26 | end 27 | 28 | it "can call a simple virtual method on a new model on the server with data" do 29 | React::IsomorphicHelpers.load_context 30 | new_user = User.new 31 | new_user.first_name = "Joe" 32 | new_user.last_name = "Schmoe" 33 | ReactiveRecord.load do 34 | new_user.detailed_name 35 | end.then { |virtual_answer| expect(virtual_answer).to eq("J. Schmoe") } 36 | end 37 | 38 | it "can call a simple virtual method on an existing updated model on the server" do 39 | React::IsomorphicHelpers.load_context 40 | user = User.find(1) 41 | user.first_name = "Joe" 42 | user.last_name = "Schmoe" 43 | ReactiveRecord.load do 44 | user.detailed_name 45 | end.then { |virtual_answer| expect(virtual_answer).to eq("J. Schmoe - mitch@catprint.com (2 todos)") } 46 | end 47 | 48 | it "can call a simple virtual method involving an existing record and a new record" do 49 | React::IsomorphicHelpers.load_context 50 | new_record = TodoItem.new 51 | ReactiveRecord.load do 52 | existing_record = User.find("1") 53 | new_record.user = existing_record 54 | new_record.virtual_user_first_name 55 | end.then { |virtual_answer| expect(virtual_answer).to eq("Mitch") } 56 | end 57 | 58 | it "can call a simple virtual method on a new model on the server with data and an updated association" do 59 | React::IsomorphicHelpers.load_context 60 | new_user = User.new 61 | new_user.first_name = "Joe" 62 | new_user.last_name = "Schmoe" 63 | todo_item = TodoItem.new 64 | todo_item.title = "Mongo DB" 65 | new_user.todo_items << TodoItem.new #todo_item 66 | ReactiveRecord.load do 67 | new_user.detailed_name 68 | end.then { |virtual_answer| expect(virtual_answer).to eq("J. Schmoe (1 todo)") } 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= javascript_include_tag 'es5-shim.min' %> 7 | <%= javascript_include_tag @server.main %> 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/spec_helper.js.rb: -------------------------------------------------------------------------------- 1 | require 'opal' 2 | require 'opal-rspec' 3 | require 'reactive_record_config' 4 | require 'react' #_js_test_only' 5 | require 'reactrb' 6 | require 'reactive-record' 7 | require 'jquery' 8 | require 'opal-jquery' 9 | require 'jquery.cookie' 10 | require 'models' 11 | 12 | 13 | Document.ready? do 14 | `$.cookie('acting_user', null, { path: '/' })` 15 | Opal::RSpec::Runner.autorun rescue nil 16 | end 17 | 18 | def sequenced_asyncs? 19 | return true 20 | #Opal::RSpec::Runner.method_defined?(auto_run) 21 | #ruby_version = RUBY_ENGINE_VERSION.split(".") 22 | #ruby_version[0].to_i > 0 or ruby_version[1].to_i > 8 23 | end 24 | 25 | class Object 26 | 27 | def use_case(*args, &block) 28 | describe(*args) do 29 | clear_opal_rspec_runners 30 | instance_eval &block 31 | last_promise = nil 32 | it "test starting" do 33 | expect(true).to be_truthy 34 | end 35 | runners = opal_rspec_runners 36 | runners = runners.reverse unless sequenced_asyncs? 37 | runners.each do |type, title, opts, block, promise| 38 | promise_to_resolve = last_promise 39 | async(title, opts) do 40 | promise.then do 41 | message = " running #{type.gsub('_',' ')} #{title}" 42 | `console.warn(#{message})` 43 | Opal::RSpec::AsyncHelpers::ClassMethods.set_current_promise self, promise_to_resolve 44 | begin 45 | instance_eval &block if block 46 | rescue Exception => e 47 | message = "Failed to run #{type} #{title}\nTest raised exception before starting test block: #{e}" 48 | `console.error(#{message})` 49 | end 50 | end 51 | end 52 | last_promise = promise 53 | last_promise.resolve if sequenced_asyncs? 54 | end 55 | last_promise.resolve unless sequenced_asyncs? 56 | end 57 | end 58 | 59 | end 60 | 61 | module Opal 62 | module RSpec 63 | module AsyncHelpers 64 | module ClassMethods 65 | 66 | def self.set_current_promise(instance, promise) 67 | @current_promise = promise 68 | @current_promise_test_instance = instance 69 | end 70 | 71 | def self.resolve_current_promise 72 | @current_promise.resolve if !sequenced_asyncs? && @current_promise 73 | rescue Exception => e 74 | raise "test structure error: Usually this is caused by a use_case test that has only a first_it an no other tests. Check the use_case that ran just before this one." 75 | end 76 | 77 | def self.get_current_promise_test_instance 78 | @current_promise_test_instance 79 | end 80 | 81 | #alias_method :old_it, :it 82 | 83 | #def it(*args, &block) 84 | # @previous_promise = new_promise 85 | # old_it(*args, &block) 86 | #end 87 | 88 | def opal_rspec_runners 89 | @opal_rspec_runners 90 | end 91 | 92 | def clear_opal_rspec_runners 93 | @opal_rspec_runners = [] 94 | end 95 | 96 | def opal_rspec_push_runner(type, title, opts, block) 97 | @opal_rspec_runners << [type, title, opts, block, Promise.new] 98 | end 99 | 100 | 101 | def first_it(title, opts = {}, &block) 102 | opal_rspec_push_runner("first_it", title, opts, block) 103 | end 104 | 105 | def now_it(title, opts = {}, &block) 106 | opal_rspec_push_runner("now_it", title, opts, block) 107 | end 108 | 109 | def and_it(title, opts = {}, &block) 110 | opal_rspec_push_runner("and_it", title, opts, block) 111 | end 112 | 113 | def finally(title, opts = {}, &block) 114 | opal_rspec_push_runner("finally", title, opts, block) 115 | end 116 | 117 | def rendering(title, &block) 118 | klass = Class.new do 119 | 120 | include React::Component 121 | 122 | def self.block 123 | @block 124 | end 125 | 126 | def self.name 127 | "dummy class" 128 | end 129 | 130 | backtrace :on 131 | 132 | def render 133 | instance_eval &self.class.block 134 | end 135 | 136 | def self.should_generate(opts={}, &block) 137 | sself = self 138 | @self.async(@title, opts) do 139 | expect_component_to_eventually(sself, &block) 140 | end 141 | end 142 | 143 | def self.should_immediately_generate(opts={}, &block) 144 | sself = self 145 | @self.it(@title, opts) do 146 | element = build_element sself, {} 147 | context = block.arity > 0 ? self : element 148 | expect((element and context.instance_exec(element, &block))).to be(true) 149 | end 150 | end 151 | 152 | end 153 | klass.instance_variable_set("@block", block) 154 | klass.instance_variable_set("@self", self) 155 | klass.instance_variable_set("@title", "it can render #{title}") 156 | klass 157 | end 158 | end 159 | end 160 | end 161 | end 162 | 163 | module ReactTestHelpers 164 | 165 | `var ReactTestUtils = React.addons.TestUtils` 166 | 167 | def renderToDocument(type, options = {}) 168 | element = React.create_element(type, options) 169 | return renderElementToDocument(element) 170 | end 171 | 172 | def renderElementToDocument(element) 173 | instance = Native(`ReactTestUtils.renderIntoDocument(#{element.to_n})`) 174 | instance.class.include(React::Component::API) 175 | return instance 176 | end 177 | 178 | def simulateEvent(event, element, params = {}) 179 | simulator = Native(`ReactTestUtils.Simulate`) 180 | #element = `#{element.to_n}.getDOMNode` unless element.class == Element 181 | simulator[event.to_s].call(element.dom_node, params) 182 | #simulator[event.to_s].call(element, params) 183 | end 184 | 185 | def isElementOfType(element, type) 186 | `React.addons.TestUtils.isElementOfType(#{element.to_n}, #{type.cached_component_class})` 187 | end 188 | 189 | def build_element(type, options) 190 | component = React.create_element(type, options) 191 | element = `ReactTestUtils.renderIntoDocument(#{component.to_n})` 192 | 193 | if !(`typeof ReactDOM === 'undefined' || typeof ReactDOM.findDOMNode === 'undefined'`) 194 | `$(ReactDOM.findDOMNode(element))` # v0.14.0 195 | elsif !(`typeof React.findDOMNode === 'undefined'`) 196 | `$(React.findDOMNode(element))` # v0.13.0 197 | else 198 | `$(element.getDOMNode())` # v0.12.0 199 | end 200 | end 201 | 202 | def expect_component_to_eventually(component_class, opts = {}, &block) 203 | # Calls block after each update of a component until it returns true. When it does set the expectation to true. 204 | # Uses the after_update callback of the component_class, then instantiates an element of that class 205 | # The call back is only called on updates, so the call back is manually called right after the 206 | # element is created. 207 | # Because React.rb runs the callback inside the components context, we have to 208 | # setup a lambda to get back to correct context before executing run_async. 209 | # Because run_async can only be run once it is protected by clearing element once the test passes. 210 | element = nil 211 | check_block = lambda do 212 | context = block.arity > 0 ? self : element 213 | run_async do 214 | element = nil; expect(true).to be(true) 215 | end if element and context.instance_exec(element, &block) 216 | end 217 | component_class.after_update { check_block.call } 218 | element = build_element component_class, opts 219 | check_block.call 220 | end 221 | 222 | def test(&block) 223 | Promise.new.tap do |promise| 224 | promise.then_test &block 225 | promise.resolve 226 | end 227 | end 228 | 229 | # for the permissions test 230 | 231 | def set_acting_user(email) 232 | `$.cookie('acting_user', #{email}, { path: '/' })` 233 | end 234 | 235 | end 236 | 237 | class Promise 238 | 239 | def then_test(&block) 240 | self.then do |args| 241 | Opal::RSpec::AsyncHelpers::ClassMethods.get_current_promise_test_instance.run_async do 242 | yield args 243 | Opal::RSpec::AsyncHelpers::ClassMethods.resolve_current_promise 244 | end 245 | end 246 | end 247 | 248 | def while_waiting(&block) 249 | if sequenced_asyncs? 250 | self.then_test {Opal::RSpec::AsyncHelpers::ClassMethods.get_current_promise_test_instance.run_async { expect(true).to be_truthy }} 251 | block.call 252 | else 253 | self.then do 254 | Opal::RSpec::AsyncHelpers::ClassMethods.resolve_current_promise 255 | end 256 | Opal::RSpec::AsyncHelpers::ClassMethods.get_current_promise_test_instance.run_async &block 257 | end 258 | end 259 | 260 | end 261 | 262 | RSpec.configure do |config| 263 | config.run_all_when_everything_filtered = true 264 | config.filter_run_including only: true 265 | config.include ReactTestHelpers 266 | config.before(:each) do 267 | `current_state = {}` 268 | end 269 | end 270 | -------------------------------------------------------------------------------- /spec/test_app/spec-opal/vendor/es5-shim.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/es-shims/es5-shim 3 | * @license es5-shim Copyright 2009-2014 by contributors, MIT License 4 | * see https://github.com/es-shims/es5-shim/blob/v4.1.0/LICENSE 5 | */ 6 | (function(t,e){"use strict";if(typeof define==="function"&&define.amd){define(e)}else if(typeof exports==="object"){module.exports=e()}else{t.returnExports=e()}})(this,function(){var t=Array.prototype;var e=Object.prototype;var r=Function.prototype;var n=String.prototype;var i=Number.prototype;var a=t.slice;var o=t.splice;var u=t.push;var l=t.unshift;var f=r.call;var s=e.toString;var c=Array.isArray||function ye(t){return s.call(t)==="[object Array]"};var p=typeof Symbol==="function"&&typeof Symbol.toStringTag==="symbol";var h;var v=Function.prototype.toString,g=function de(t){try{v.call(t);return true}catch(e){return false}},y="[object Function]",d="[object GeneratorFunction]";h=function me(t){if(typeof t!=="function"){return false}if(p){return g(t)}var e=s.call(t);return e===y||e===d};var m;var b=RegExp.prototype.exec,w=function be(t){try{b.call(t);return true}catch(e){return false}},T="[object RegExp]";m=function we(t){if(typeof t!=="object"){return false}return p?w(t):s.call(t)===T};var x;var O=String.prototype.valueOf,j=function Te(t){try{O.call(t);return true}catch(e){return false}},S="[object String]";x=function xe(t){if(typeof t==="string"){return true}if(typeof t!=="object"){return false}return p?j(t):s.call(t)===S};var E=function Oe(t){var e=s.call(t);var r=e==="[object Arguments]";if(!r){r=!c(t)&&t!==null&&typeof t==="object"&&typeof t.length==="number"&&t.length>=0&&h(t.callee)}return r};var N=function(t){var e=Object.defineProperty&&function(){try{Object.defineProperty({},"x",{});return true}catch(t){return false}}();var r;if(e){r=function(t,e,r,n){if(!n&&e in t){return}Object.defineProperty(t,e,{configurable:true,enumerable:false,writable:true,value:r})}}else{r=function(t,e,r,n){if(!n&&e in t){return}t[e]=r}}return function n(e,i,a){for(var o in i){if(t.call(i,o)){r(e,o,i[o],a)}}}}(e.hasOwnProperty);function I(t){var e=typeof t;return t===null||e==="undefined"||e==="boolean"||e==="number"||e==="string"}var D={ToInteger:function je(t){var e=+t;if(e!==e){e=0}else if(e!==0&&e!==1/0&&e!==-(1/0)){e=(e>0||-1)*Math.floor(Math.abs(e))}return e},ToPrimitive:function Se(t){var e,r,n;if(I(t)){return t}r=t.valueOf;if(h(r)){e=r.call(t);if(I(e)){return e}}n=t.toString;if(h(n)){e=n.call(t);if(I(e)){return e}}throw new TypeError},ToObject:function(t){if(t==null){throw new TypeError("can't convert "+t+" to object")}return Object(t)},ToUint32:function Ee(t){return t>>>0}};var M=function Ne(){};N(r,{bind:function Ie(t){var e=this;if(!h(e)){throw new TypeError("Function.prototype.bind called on incompatible "+e)}var r=a.call(arguments,1);var n;var i=function(){if(this instanceof n){var i=e.apply(this,r.concat(a.call(arguments)));if(Object(i)===i){return i}return this}else{return e.apply(t,r.concat(a.call(arguments)))}};var o=Math.max(0,e.length-r.length);var u=[];for(var l=0;l0&&typeof e!=="number"){r=a.call(arguments);if(r.length<2){r.push(this.length-t)}else{r[1]=D.ToInteger(e)}}return o.apply(this,r)}},!U);var k=[].unshift(0)!==1;N(t,{unshift:function(){l.apply(this,arguments);return this.length}},k);N(Array,{isArray:c});var A=Object("a");var C=A[0]!=="a"||!(0 in A);var P=function Fe(t){var e=true;var r=true;if(t){t.call("foo",function(t,r,n){if(typeof n!=="object"){e=false}});t.call([1],function(){"use strict";r=typeof this==="string"},"x")}return!!t&&e&&r};N(t,{forEach:function Re(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=arguments[1],i=-1,a=r.length>>>0;if(!h(t)){throw new TypeError}while(++i>>0,i=Array(n),a=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var o=0;o>>0,i=[],a,o=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var u=0;u>>0,i=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var a=0;a>>0,i=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var a=0;a>>0;if(!h(t)){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduce of empty array with no initial value")}var i=0;var a;if(arguments.length>=2){a=arguments[1]}else{do{if(i in r){a=r[i++];break}if(++i>=n){throw new TypeError("reduce of empty array with no initial value")}}while(true)}for(;i>>0;if(!h(t)){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduceRight of empty array with no initial value")}var i,a=n-1;if(arguments.length>=2){i=arguments[1]}else{do{if(a in r){i=r[a--];break}if(--a<0){throw new TypeError("reduceRight of empty array with no initial value")}}while(true)}if(a<0){return i}do{if(a in r){i=t.call(void 0,i,r[a],a,e)}}while(a--);return i}},!J);var z=Array.prototype.indexOf&&[0,1].indexOf(1,2)!==-1;N(t,{indexOf:function Je(t){var e=C&&x(this)?this.split(""):D.ToObject(this),r=e.length>>>0;if(!r){return-1}var n=0;if(arguments.length>1){n=D.ToInteger(arguments[1])}n=n>=0?n:Math.max(0,r+n);for(;n>>0;if(!r){return-1}var n=r-1;if(arguments.length>1){n=Math.min(n,D.ToInteger(arguments[1]))}n=n>=0?n:r-Math.abs(n);for(;n>=0;n--){if(n in e&&t===e[n]){return n}}return-1}},$);var B=!{toString:null}.propertyIsEnumerable("toString"),G=function(){}.propertyIsEnumerable("prototype"),H=!F("x","0"),L=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],X=L.length;N(Object,{keys:function $e(t){var e=h(t),r=E(t),n=t!==null&&typeof t==="object",i=n&&x(t);if(!n&&!e&&!r){throw new TypeError("Object.keys called on a non-object")}var a=[];var o=G&&e;if(i&&H||r){for(var u=0;u9999?"+":"")+("00000"+Math.abs(n)).slice(0<=n&&n<=9999?-4:-6);e=t.length;while(e--){r=t[e];if(r<10){t[e]="0"+r}}return n+"-"+t.slice(0,2).join("-")+"T"+t.slice(2).join(":")+"."+("000"+this.getUTCMilliseconds()).slice(-3)+"Z"}},V);var W=false;try{W=Date.prototype.toJSON&&new Date(NaN).toJSON()===null&&new Date(K).toJSON().indexOf(Q)!==-1&&Date.prototype.toJSON.call({toISOString:function(){return true}})}catch(_){}if(!W){Date.prototype.toJSON=function He(t){var e=Object(this),r=D.ToPrimitive(e),n;if(typeof r==="number"&&!isFinite(r)){return null}n=e.toISOString;if(typeof n!=="function"){throw new TypeError("toISOString property is not callable")}return n.call(e)}}var te=Date.parse("+033658-09-27T01:46:40.000Z")===1e15;var ee=!isNaN(Date.parse("2012-04-04T24:00:00.500Z"))||!isNaN(Date.parse("2012-11-31T23:59:59.000Z"));var re=isNaN(Date.parse("2000-01-01T00:00:00.000Z"));if(!Date.parse||re||ee||!te){Date=function(t){function e(r,n,i,a,o,u,l){var f=arguments.length;if(this instanceof t){var s=f===1&&String(r)===r?new t(e.parse(r)):f>=7?new t(r,n,i,a,o,u,l):f>=6?new t(r,n,i,a,o,u):f>=5?new t(r,n,i,a,o):f>=4?new t(r,n,i,a):f>=3?new t(r,n,i):f>=2?new t(r,n):f>=1?new t(r):new t;s.constructor=e;return s}return t.apply(this,arguments)}var r=new RegExp("^"+"(\\d{4}|[+-]\\d{6})"+"(?:-(\\d{2})"+"(?:-(\\d{2})"+"(?:"+"T(\\d{2})"+":(\\d{2})"+"(?:"+":(\\d{2})"+"(?:(\\.\\d{1,}))?"+")?"+"("+"Z|"+"(?:"+"([-+])"+"(\\d{2})"+":(\\d{2})"+")"+")?)?)?)?"+"$");var n=[0,31,59,90,120,151,181,212,243,273,304,334,365];function i(t,e){var r=e>1?1:0;return n[e]+Math.floor((t-1969+r)/4)-Math.floor((t-1901+r)/100)+Math.floor((t-1601+r)/400)+365*(t-1970)}function a(e){return Number(new t(1970,0,1,0,0,0,e))}for(var o in t){e[o]=t[o]}e.now=t.now;e.UTC=t.UTC;e.prototype=t.prototype;e.prototype.constructor=e;e.parse=function u(e){var n=r.exec(e);if(n){var o=Number(n[1]),u=Number(n[2]||1)-1,l=Number(n[3]||1)-1,f=Number(n[4]||0),s=Number(n[5]||0),c=Number(n[6]||0),p=Math.floor(Number(n[7]||0)*1e3),h=Boolean(n[4]&&!n[8]),v=n[9]==="-"?1:-1,g=Number(n[10]||0),y=Number(n[11]||0),d;if(f<(s>0||c>0||p>0?24:25)&&s<60&&c<60&&p<1e3&&u>-1&&u<12&&g<24&&y<60&&l>-1&&l=0){r+=ie.data[e];ie.data[e]=Math.floor(r/t);r=r%t*ie.base}},numToString:function qe(){var t=ie.size;var e="";while(--t>=0){if(e!==""||t===0||ie.data[t]!==0){var r=String(ie.data[t]);if(e===""){e=r}else{e+="0000000".slice(0,7-r.length)+r}}}return e},pow:function Ke(t,e,r){return e===0?r:e%2===1?Ke(t,e-1,r*t):Ke(t*t,e/2,r)},log:function Qe(t){var e=0;while(t>=4096){e+=12;t/=4096}while(t>=2){e+=1;t/=2}return e}};N(i,{toFixed:function Ve(t){var e,r,n,i,a,o,u,l;e=Number(t);e=e!==e?0:Math.floor(e);if(e<0||e>20){throw new RangeError("Number.toFixed called with invalid number of decimals")}r=Number(this);if(r!==r){return"NaN"}if(r<=-1e21||r>=1e21){return String(r)}n="";if(r<0){n="-";r=-r}i="0";if(r>1e-21){a=ie.log(r*ie.pow(2,69,1))-69;o=a<0?r*ie.pow(2,-a,1):r/ie.pow(2,a,1);o*=4503599627370496;a=52-a;if(a>0){ie.multiply(0,o);u=e;while(u>=7){ie.multiply(1e7,0);u-=7}ie.multiply(ie.pow(10,u,1),0);u=a-1;while(u>=23){ie.divide(1<<23);u-=23}ie.divide(1<0){l=i.length;if(l<=e){i=n+"0.0000000000000000000".slice(0,e-l+2)+i}else{i=n+i.slice(0,l-e)+"."+i.slice(l-e)}}else{i=n+i}return i}},ne);var ae=n.split;if("ab".split(/(?:ab)*/).length!==2||".".split(/(.?)(.?)/).length!==4||"tesst".split(/(s)*/)[1]==="t"||"test".split(/(?:)/,-1).length!==4||"".split(/.?/).length||".".split(/()()/).length>1){(function(){var t=typeof/()??/.exec("")[1]==="undefined";n.split=function(e,r){var n=this;if(typeof e==="undefined"&&r===0){return[]}if(!m(e)){return ae.call(this,e,r)}var i=[],a=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.extended?"x":"")+(e.sticky?"y":""),o=0,l,f,s,c;e=new RegExp(e.source,a+"g");n+="";if(!t){l=new RegExp("^"+e.source+"$(?!\\s)",a)}r=typeof r==="undefined"?-1>>>0:D.ToUint32(r);f=e.exec(n);while(f){s=f.index+f[0].length;if(s>o){i.push(n.slice(o,f.index));if(!t&&f.length>1){f[0].replace(l,function(){for(var t=1;t1&&f.index=r){break}}if(e.lastIndex===f.index){e.lastIndex++}f=e.exec(n)}if(o===n.length){if(c||!e.test("")){i.push("")}}else{i.push(n.slice(o))}return i.length>r?i.slice(0,r):i}})()}else if("0".split(void 0,0).length){n.split=function We(t,e){if(typeof t==="undefined"&&e===0){return[]}return ae.call(this,t,e)}}var oe=n.replace;var ue=function(){var t=[];"x".replace(/x(.)?/g,function(e,r){t.push(r)});return t.length===1&&typeof t[0]==="undefined"}();if(!ue){n.replace=function _e(t,e){var r=h(e);var n=m(t)&&/\)[*?]/.test(t.source);if(!r||!n){return oe.call(this,t,e)}else{var i=function(r){var n=arguments.length;var i=t.lastIndex;t.lastIndex=0;var a=t.exec(r)||[];t.lastIndex=i;a.push(arguments[n-2],arguments[n-1]);return e.apply(this,a)};return oe.call(this,t,i)}}}var le=n.substr;var fe="".substr&&"0b".substr(-1)!=="b";N(n,{substr:function tr(t,e){return le.call(this,t<0?(t=this.length+t)<0?0:t:t,e)}},fe);var se=" \n \f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003"+"\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028"+"\u2029\ufeff";var ce="\u200b";var pe="["+se+"]";var he=new RegExp("^"+pe+pe+"*");var ve=new RegExp(pe+pe+"*$");var ge=n.trim&&(se.trim()||!ce.trim());N(n,{trim:function er(){if(typeof this==="undefined"||this===null){throw new TypeError("can't convert "+this+" to object")}return String(this).replace(he,"").replace(ve,"")}},ge);if(parseInt(se+"08")!==8||parseInt(se+"0x16")!==22){parseInt=function(t){var e=/^0[xX]/;return function r(n,i){n=String(n).trim();if(!Number(i)){i=e.test(n)?16:10}return t(n,i)}}(parseInt)}}); 7 | -------------------------------------------------------------------------------- /spec/test_app/spec/server_unit_tests/pry_rescue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "ServerDataCache" do 4 | 5 | before(:all) do 6 | @current_pry_definition = Object.const_get("Pry") if defined? Pry 7 | @current_pry_rescue_definition = Object.const_get("PryRescue") if defined? PryRescue 8 | end 9 | 10 | after(:all) do 11 | Object.const_set("Pry", @current_pry_definition) if @current_pry_definition 12 | Object.const_set("PryRescue", @current_pry_definition) if @current_pry_rescue_definition 13 | end 14 | 15 | it "behaves normally if there is no pry rescue" do 16 | expect { ReactiveRecord::ServerDataCache[[],[], [["User", ["find", 1], "fake_attribute"]], nil] }.to raise_error(ActiveRecord::RecordNotFound) 17 | end 18 | 19 | context "will use pry rescue if it is defined" do 20 | 21 | before(:all) do 22 | pry = Class.new do 23 | def self.rescue 24 | yield 25 | end 26 | def self.rescued(e) 27 | @last_exception = e 28 | end 29 | def self.last_exception 30 | @last_exception 31 | end 32 | end 33 | Object.const_set("PryRescue", true) 34 | Object.const_set("Pry", pry) 35 | end 36 | 37 | it "and it will still raise an error" do 38 | expect { ReactiveRecord::ServerDataCache[[],[], [["User", ["find", 1], "fake_attribute"]], nil] }.to raise_error(ActiveRecord::RecordNotFound) 39 | end 40 | 41 | it "but it will call Pry.rescued first" do 42 | ReactiveRecord::ServerDataCache[[],[], ["User", ["new", 10852], "fake_attribute"], nil] rescue nil 43 | expect(Pry.last_exception).to be_a(Exception) 44 | end 45 | 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /spec/test_app/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= 'test' 2 | require File.expand_path("../../config/environment", __FILE__) 3 | require 'rspec/rails' 4 | 5 | #require 'rspec' 6 | #require 'rspec/expectations' 7 | #require 'shoulda/matchers' 8 | #require 'database_cleaner' 9 | # require "bundler/setup" 10 | # Bundler.setup 11 | # require "rails" 12 | require 'reactive-record' 13 | 14 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 15 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 16 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 17 | # file to always be loaded, without a need to explicitly require it in any files. 18 | # 19 | # Given that it is always loaded, you are encouraged to keep this file as 20 | # light-weight as possible. Requiring heavyweight dependencies from this file 21 | # will add to the boot time of your test suite on EVERY test run, even for an 22 | # individual file that may not need all of that loaded. Instead, consider making 23 | # a separate helper file that requires the additional dependencies and performs 24 | # the additional setup, and require it from the spec files that actually need it. 25 | # 26 | # The `.rspec` file also contains a few flags that are not defaults but that 27 | # users commonly want. 28 | # 29 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 30 | RSpec.configure do |config| 31 | # rspec-expectations config goes here. You can use an alternate 32 | # assertion/expectation library such as wrong or the stdlib/minitest 33 | # assertions if you prefer. 34 | config.expect_with :rspec do |expectations| 35 | # Enable only the newer, non-monkey-patching expect syntax. 36 | # For more details, see: 37 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 38 | expectations.syntax = [:should, :expect] 39 | end 40 | 41 | # rspec-mocks config goes here. You can use an alternate test double 42 | # library (such as bogus or mocha) by changing the `mock_with` option here. 43 | config.mock_with :rspec do |mocks| 44 | # Enable only the newer, non-monkey-patching expect syntax. 45 | # For more details, see: 46 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 47 | mocks.syntax = :expect 48 | 49 | # Prevents you from mocking or stubbing a method that does not exist on 50 | # a real object. This is generally recommended. 51 | mocks.verify_partial_doubles = true 52 | end 53 | 54 | #config.use_transactional_fixtures = false 55 | 56 | # DatabaseCleaner.strategy = :truncation 57 | 58 | # config.before(:suite) do 59 | # begin 60 | # DatabaseCleaner.clean 61 | # DatabaseCleaner.start 62 | # FactoryGirl.lint 63 | # ensure 64 | # DatabaseCleaner.clean 65 | # end 66 | # end 67 | 68 | # config.after(:suite) do 69 | # DatabaseCleaner.clean 70 | # end 71 | 72 | # config.before(:suite) do 73 | # DatabaseCleaner.clean_with(:truncation) 74 | # end 75 | # 76 | # config.before(:each) do 77 | # DatabaseCleaner.strategy = :transaction 78 | # end 79 | # 80 | # config.before(:each, :js => true) do 81 | # DatabaseCleaner.strategy = :truncation 82 | # end 83 | # 84 | # config.before(:each) do 85 | # DatabaseCleaner.start 86 | # end 87 | # 88 | # config.after(:each) do 89 | # DatabaseCleaner.clean 90 | # end 91 | 92 | # The settings below are suggested to provide a good initial experience 93 | # with RSpec, but feel free to customize to your heart's content. 94 | # =begin 95 | # # These two settings work together to allow you to limit a spec run 96 | # # to individual examples or groups you care about by tagging them with 97 | # # `:focus` metadata. When nothing is tagged with `:focus`, all examples 98 | # # get run. 99 | # config.filter_run :focus 100 | # config.run_all_when_everything_filtered = true 101 | # 102 | # # Limits the available syntax to the non-monkey patched syntax that is recommended. 103 | # # For more details, see: 104 | # # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 105 | # # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 106 | # # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 107 | # config.disable_monkey_patching! 108 | # 109 | # # Many RSpec users commonly either run the entire suite or an individual 110 | # # file, and it's useful to allow more verbose output when running an 111 | # # individual spec file. 112 | # if config.files_to_run.one? 113 | # # Use the documentation formatter for detailed output, 114 | # # unless a formatter has already been configured 115 | # # (e.g. via a command-line flag). 116 | # config.default_formatter = 'doc' 117 | # end 118 | # 119 | # # Print the 10 slowest examples and example groups at the 120 | # # end of the spec run, to help surface which specs are running 121 | # # particularly slow. 122 | # config.profile_examples = 10 123 | # 124 | # # Run specs in random order to surface order dependencies. If you find an 125 | # # order dependency and want to debug it, you can fix the order by providing 126 | # # the seed, which is printed after each run. 127 | # # --seed 1234 128 | # config.order = :random 129 | # 130 | # # Seed global randomization in this process using the `--seed` CLI option. 131 | # # Setting this allows you to use `--seed` to deterministically reproduce 132 | # # test failures related to randomization by passing the same `--seed` value 133 | # # as the one that triggered the failure. 134 | # Kernel.srand config.seed 135 | # =end 136 | 137 | end 138 | --------------------------------------------------------------------------------