constant_record 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0c54e7e14d065796037c8c68789bf16ed980cbdc
4
+ data.tar.gz: e2756dc08f225ce136ac3f07dc20d1b773fa579e
5
+ SHA512:
6
+ metadata.gz: 56847eabe09d8c35111905feca49d35e8dafc5ea70226b213e05c116ea01b75d61709aa6e553e3aa5902a22e7a910232082cf3c7663cd1f2605b64f4f254059d
7
+ data.tar.gz: d5c59bf34e24b0ab5b7fbfcd509de8c5e006cf164528dada95de3a6183486f412c40b4214a1eafcd5be97676c0975f6a9f9030ea2d776070bac7b0cada8bb6e8
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in constant_record.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nate Wiger
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # ConstantRecord
2
+
3
+ In-memory ActiveRecord querying and associations for static data.
4
+
5
+ Compatible with all current versions of Rails, from 3.x up through 4.1
6
+ (and beyond, theoretically).
7
+
8
+ Unlike previous (ambitious) approaches that have tried to duplicate ActiveRecord
9
+ functionality in a separate set of classes, this is a simple shim of < 200 LOC
10
+ that creates an in-memory SQLite database for the relevant models. This is designed
11
+ to minimize breakage between Rails versions, and also avoids recreating ActiveRecord
12
+ features.
13
+
14
+ ## Installation
15
+
16
+ Add `constant_record` to your Gemfile:
17
+
18
+ gem 'constant_record'
19
+
20
+ Then run `bundle install`. Or, install it yourself manually, if you're into that sort of thing:
21
+
22
+ $ gem install constant_record
23
+
24
+ *Note: The gem name is constant_record with an underscore, unlike activerecord.*
25
+
26
+ ## Usage
27
+
28
+ Just `include ConstantRecord` in any ActiveRecord class. Then, you can use `data` to add
29
+ data directly in that class for clarity:
30
+
31
+ class Genre < ActiveRecord::Base
32
+ include ConstantRecord
33
+
34
+ data id: 1, name: "Rock", slug: "rock"
35
+ data id: 2, name: "Hip-Hop", slug: "hiphop"
36
+ data id: 3, name: "Pop", slug: "pop"
37
+ end
38
+
39
+ Or, you can choose to keep your data in a YAML file:
40
+
41
+ class Genre < ActiveRecord::Base
42
+ include ConstantRecord
43
+ load_data File.join(Rails.root, 'config', 'data', 'genres.yml')
44
+ end
45
+
46
+ The YAML file should be an array of hashes:
47
+
48
+ # config/data/genres.yml
49
+ ---
50
+ - id: 1
51
+ name: Rock
52
+ slug: rock
53
+ - id: 2
54
+ name: Hip-Hop
55
+ slug: hiphop
56
+ - id: 3
57
+ name: Pop
58
+ slug: hop
59
+
60
+ You can omit the filename if it follows the naming convention of `config/data/[table_name].yml`:
61
+
62
+ class Genre < ActiveRecord::Base
63
+ include ConstantRecord
64
+ load_data # config/data/genres.yml
65
+ end
66
+
67
+ Alternatively, you can load your data via some other external method. Note that you will need
68
+ to reload your data each time Rails restarts, since the data is in-memory only. This means
69
+ adding a reload hook after Unicorn / Passenger / Puma fork:
70
+
71
+ Genre.reload!
72
+
73
+ Once you define your class, this will create an in-memory `sqlite` database which is then
74
+ hooked into ActiveRecord. A database table is created on the fly, consisting of the columns
75
+ you use in the *first* `data` declaration. **Important:** This means if you have a couple
76
+ columns that aren't always present, *make sure to include them with `column_name: nil` on
77
+ the first `data` line:*
78
+
79
+ class Genre < ActiveRecord::Base
80
+ include ConstantRecord
81
+
82
+ data id: 1, name: "Rock", slug: "rock", region: nil, country: nil
83
+ data id: 2, name: "Hip-Hop", slug: "hiphop", region: 'North America'
84
+ data id: 3, name: "Pop", slug: "pop", country: 'US'
85
+ end
86
+
87
+ Once setup, all the familiar ActiveRecord finders work:
88
+
89
+ Genre.find(1)
90
+ Genre.find_by_slug("pop")
91
+ Genre.where(name: "Rock").first
92
+
93
+ Attempts to modify values will fail:
94
+
95
+ @genre = Genre.find(2)
96
+ @genre.slug = "hip-hop"
97
+ @genre.save! # nope
98
+
99
+ You'll get an `ActiveRecord::ReadOnlyRecord` exception.
100
+
101
+ ## Auto Constants
102
+
103
+ ConstantRecord will also create constants on the fly for you if you have a `name` column.
104
+ Revisiting our example:
105
+
106
+ class Genre < ActiveRecord::Base
107
+ include ConstantRecord
108
+
109
+ data id: 1, name: "Rock", slug: "rock"
110
+ data id: 2, name: "Hip-Hop", slug: "hiphop"
111
+ data id: 3, name: "Pop", slug: "pop"
112
+ end
113
+
114
+ This will create:
115
+
116
+ Genre::ROCK = 1
117
+ Genre::HIP_HOP = 2
118
+ Genre::POP = 3
119
+
120
+ This makes it cleaner to do queries in your app:
121
+
122
+ Genre.find(Genre::ROCK)
123
+ Song.where(genre_id: Genre::ROCK)
124
+
125
+ And so on.
126
+
127
+ ## Associations
128
+
129
+ Internally, ActiveRecord tries to do joins to retrieve associations. This doesn't work, since
130
+ the records live in different tables. Have no fear, you just need to `include ConstantRecord::Associations`
131
+ in the normal ActiveRecord class that is trying to associate to your ConstantRecord class:
132
+
133
+ class Genre < ActiveRecord::Base
134
+ include ConstantRecord
135
+
136
+ has_many :song_genres
137
+ has_many :songs, through: :song_genres
138
+
139
+ data id: 1, name: "Rock", slug: "rock", region: nil, country: nil
140
+ data id: 2, name: "Hip-Hop", slug: "hiphop", region: 'North America'
141
+ data id: 3, name: "Pop", slug: "pop", country: 'US'
142
+ end
143
+
144
+ class SongGenre < ActiveRecord::Base
145
+ belongs_to :genre_id
146
+ belongs_to :song_id
147
+ end
148
+
149
+ class Song < ActiveRecord::Base
150
+ include ConstantRecord::Associations
151
+ has_many :song_genres
152
+ has_many :songs, through: :song_genres
153
+ end
154
+
155
+ If you forget to do this, you'll get an error like this:
156
+
157
+ irb(main):001:0> @song = Song.first
158
+ irb(main):002:0> @song.genres
159
+ ActiveRecord::StatementInvalid: Could not find table 'song_genres'
160
+
161
+ It would be great to remove this shim, but I can't currently see a way without monkey-patching
162
+ the internals of ActiveRecord, which I don't want to do for 17 different reasons.
163
+
164
+ ## Debugging
165
+
166
+ If you forget to define data, you'll get a "table doesn't exist" error:
167
+
168
+ class Publisher < ActiveRecord::Base
169
+ include ConstantRecord
170
+
171
+ # Oops no data
172
+
173
+ has_many :article_publishers
174
+ has_many :articles, through: :article_publishers
175
+ end
176
+
177
+ irb(main):001:0> @publisher = Publisher.first
178
+ irb(main):002:0> @publisher.articles
179
+ ActiveRecord::StatementInvalid: Could not find table 'articles'
180
+
181
+ This is because the table is created lazily when you first load data.
182
+
183
+ If you try to add a custom column on a different `data` line:
184
+
185
+ class Genre < ActiveRecord::Base
186
+ include ConstantRecord
187
+
188
+ data id: 1, name: "Rock", slug: "rock"
189
+ data id: 2, name: "Hip-Hop", slug: "hiphop"
190
+ data id: 3, name: "Pop", slug: "pop", ranking: 1 # oops
191
+ end
192
+
193
+ You'll get a table error:
194
+
195
+ ActiveRecord::UnknownAttributeError: unknown attribute: ranking
196
+
197
+ The solution is to include the same columns on each `data` line.
198
+
199
+ ## Other Projects
200
+
201
+ Inspired by a couple previous efforts:
202
+
203
+ * Christoph Petschnig's [constantrecord](https://github.com/cpetschnig/constantrecord)
204
+ * Aaron Quint's [static_model](https://github.com/quirkey/static_model)
205
+ * Nico Taing's [yaml_record](https://github.com/nicotaing/yaml_record)
206
+
207
+ Other projects seen in the wild:
208
+
209
+ * [static_record](https://github.com/dejan/static_record)
210
+ * [constant_record](https://github.com/topdan/constant_record)
211
+ * [frozen_record](https://github.com/byroot/frozen_record)
212
+
213
+ All are good efforts, but unfortunately the Rails team is known to make sweeping
214
+ changes to internal ActiveRecord implementation details between different versions
215
+ of Rails. This makes it very difficult to maintain compatibility over time.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc "run all the specs"
4
+ task :test do
5
+ sh "bacon spec/*_spec.rb"
6
+ end
7
+
8
+ task :default => :test
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'constant_record/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "constant_record"
8
+ spec.version = ConstantRecord::VERSION
9
+ spec.authors = ["Nate Wiger"]
10
+ spec.email = ["nwiger@gmail.com"]
11
+ spec.summary = %q{In-memory ActiveRecord querying and associations for static data.}
12
+ spec.description = <<-EndDesc
13
+ In-memory ActiveRecord querying and associations for static data.
14
+ Improves performance and decreases bugs due to data mismatches.
15
+ EndDesc
16
+ spec.homepage = ""
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ["lib"]
23
+
24
+ # Allow any AR versions, but need at least one. Also sqlite for the table.
25
+ spec.add_dependency "activerecord"
26
+ spec.add_dependency "sqlite3"
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.6"
29
+ spec.add_development_dependency "rake"
30
+ spec.add_development_dependency "bacon"
31
+ end
@@ -0,0 +1,3 @@
1
+ module ConstantRecord
2
+ VERSION = "0.5.0"
3
+ end
@@ -0,0 +1,220 @@
1
+ #
2
+ # To use, `include ConstantRecord` in any ActiveRecord class. Then, you can use `data`
3
+ # to add data directly in that class for clarity:
4
+ #
5
+ # class Genre < ActiveRecord::Base
6
+ # include ConstantRecord
7
+ #
8
+ # data id: 1, name: "Rock", slug: "rock"
9
+ # data id: 2, name: "Hip-Hop", slug: "hiphop"
10
+ # data id: 3, name: "Pop", slug: "pop"
11
+ # end
12
+ #
13
+ # Or, you can choose to keep your data in a YAML file:
14
+ #
15
+ # class Genre < ActiveRecord::Base
16
+ # include ConstantRecord
17
+ # load_data File.join(Rails.root, 'config', 'data', 'genres.yml')
18
+ # end
19
+ #
20
+ # The YAML file should be an array of hashes. Once initialized, all
21
+ # familiar ActiveRecord finders and associations should work as expected.
22
+ #
23
+ require "active_record"
24
+ require "constant_record/version"
25
+
26
+ module ConstantRecord
27
+ class Error < StandardError; end
28
+ class BadDataFile < Error; end
29
+
30
+ MEMORY_DBCONFIG = {adapter: 'sqlite3', database: ":memory:", pool: 5}.freeze
31
+
32
+
33
+ class << self
34
+ def memory_dbconfig
35
+ @memory_dbconfig || MEMORY_DBCONFIG
36
+ end
37
+
38
+ def memory_dbconfig=(config)
39
+ @memory_dbconfig = config
40
+ end
41
+
42
+ def data_dir
43
+ @data_dir || File.join('config', 'data')
44
+ end
45
+
46
+ def data_dir=(path)
47
+ @data_dir = path
48
+ end
49
+
50
+ def included(base)
51
+ base.extend DataLoading
52
+ base.extend Associations
53
+ base.send :include, ReadOnly
54
+ base.establish_connection(memory_dbconfig) unless base.send :connected?
55
+ end
56
+ end
57
+
58
+ #
59
+ # Loads data either directly in the model class, or from a YAML file.
60
+ #
61
+ module DataLoading
62
+ def data_file
63
+ @data_file || File.join(ConstantRecord.data_dir, "#{self.to_s.tableize}.yml")
64
+ end
65
+
66
+ def load_data(file=nil)
67
+ @data_file = file
68
+ reload!
69
+ end
70
+
71
+ def load(reload=false)
72
+ return if loaded? && !reload
73
+ records = YAML.load_file(data_file)
74
+
75
+ if !records.is_a?(Array) or records.empty?
76
+ raise BadDataFile, "Expected array in data file #{data_file}: #{records.inspect}"
77
+ end
78
+
79
+ # Call our method to populate data
80
+ records.each{|r| data r}
81
+
82
+ @loaded = true
83
+ end
84
+
85
+ def reload!
86
+ load(true)
87
+ end
88
+
89
+ def loaded?
90
+ @loaded || false
91
+ end
92
+
93
+ # Define a constant record: data id: 1, name: "California", slug: "CA"
94
+ def data(attrib)
95
+ attrib.symbolize_keys!
96
+ raise ArgumentError, "#{self}.data expects a Hash of attributes" unless attrib.is_a?(Hash)
97
+
98
+ unless attrib[primary_key.to_sym]
99
+ raise ArgumentError, "#{self}.data missing primary key '#{primary_key}': #{attrib.inspect}"
100
+ end
101
+
102
+ unless @table_was_created
103
+ create_memory_table(attrib)
104
+ @table_was_created = true
105
+ end
106
+
107
+ new_record = new(attrib)
108
+ new_record.id = attrib[primary_key.to_sym]
109
+
110
+ # Check for duplicates
111
+ if old_record = find_by_id(new_record.id)
112
+ raise ActiveRecord::RecordNotUnique,
113
+ "Duplicate #{self} id=#{new_record.id} found: #{new_record} vs #{old_record}"
114
+ end
115
+ new_record.save!
116
+
117
+ # create Ruby constants as well, so "id: 3, name: Sky" gets SKY=3
118
+ if new_record.respond_to?(:name) and name = new_record.name
119
+ const_name =
120
+ name.to_s.upcase.strip.gsub(/[-\s]+/,'_').sub(/^[0-9_]+/,'').gsub(/\W+/,'')
121
+ const_set const_name, new_record.id unless const_defined?(const_name)
122
+ end
123
+ end
124
+
125
+ protected
126
+
127
+ # Create our in-memory table based on columns we have defined in our data.
128
+ def create_memory_table(attrib)
129
+ db_columns = {}
130
+ attrib.each do |col,val|
131
+ next if col.to_s == 'id' # skip pk
132
+ db_columns[col] =
133
+ case val
134
+ when Integer then :integer
135
+ when Float then :decimal
136
+ when Date then :date
137
+ when DateTime, Time then :datetime
138
+ else :string
139
+ end
140
+ end
141
+
142
+ # Create the table in memory
143
+ connection.create_table(table_name) do |t|
144
+ db_columns.each do |col,type|
145
+ t.column col, type
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ #
152
+ # Hooks to integrate ActiveRecord associations with constant records.
153
+ #
154
+ module Associations
155
+ def self.included(base)
156
+ base.extend self # support "include" as well
157
+ end
158
+
159
+ #
160
+ # Override the default ActiveRecord.has_many(:through) that does in-database joins,
161
+ # with a method that makes two fetches. It's the only reliable way to traverse
162
+ # databases. Hopefully one (or both) of these tables are in-memory ConstantRecords
163
+ # so that we're not making real DB calls.
164
+ #
165
+ def has_many(other_table, options={})
166
+ # puts "#{self}(#{table_name}).has_many #{other_table.inspect}, #{options.inspect}"
167
+ if join_tab = options[:through]
168
+ foreign_key = options[:foreign_key] || other_table.to_s.singularize.foreign_key
169
+ prime_key = options[:primary_key] || primary_key
170
+ class_name = options[:class_name] || other_table.to_s.classify
171
+ join_key = table_name.to_s.singularize.foreign_key
172
+
173
+ define_method other_table do
174
+ join_class = join_tab.to_s.classify.constantize
175
+ ids = join_class.where(join_key => send(prime_key)).pluck(foreign_key)
176
+ return [] if ids.empty?
177
+ class_name.constantize.where(id: ids)
178
+ end
179
+ else
180
+ super other_table, options
181
+ end
182
+ end
183
+ end
184
+
185
+ #
186
+ # Raise an error if the application attempts to change constant records.
187
+ #
188
+ module ReadOnly
189
+ def self.included(base)
190
+ base.extend ClassMethods
191
+ end
192
+
193
+ def readonly?
194
+ # have to allow inserts to load_data
195
+ new_record? ? false : true
196
+ end
197
+
198
+ def delete
199
+ raise ActiveRecord::ReadOnlyRecord
200
+ end
201
+
202
+ def destroy
203
+ raise ActiveRecord::ReadOnlyRecord
204
+ end
205
+
206
+ module ClassMethods
207
+ def delete(id_or_array)
208
+ raise ActiveRecord::ReadOnlyRecord
209
+ end
210
+
211
+ def delete_all(conditions = nil)
212
+ raise ActiveRecord::ReadOnlyRecord
213
+ end
214
+
215
+ def update_all(conditions = nil)
216
+ raise ActiveRecord::ReadOnlyRecord
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,161 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "ConstantRecord" do
4
+ describe "loading data" do
5
+ it "data(attr: val)" do
6
+ date1 = Time.now
7
+ date2 = Date.new
8
+ date3 = DateTime.new
9
+
10
+ # insert out of order to ensure we can override ID
11
+ Author.data(id: 1, name: "One", birthday: date1)
12
+ Author.data(id: 3, name: "Three", birthday: date3)
13
+ Author.data(id: 2, name: "Two", birthday: date2)
14
+
15
+ Author.count.should == 3
16
+ Author.find(1).name.should == "One"
17
+ Author.find(2).name.should == "Two"
18
+ Author.find(3).name.should == "Three"
19
+
20
+ author = Author.find_by_name("One")
21
+ author.id.should == 1
22
+ author.birthday.should == date1
23
+
24
+ author = Author.find_by_name("Two")
25
+ author.id.should == 2
26
+ author.birthday.should == date2
27
+
28
+ author = Author.find_by_name("Three")
29
+ author.id.should == 3
30
+ author.birthday.should == date3
31
+ end
32
+
33
+ it "supports AR finders" do
34
+ Author.where(id: [1,2]).count.should == 2
35
+ Author.where(['name like ?', 'Three']).count.should == 1
36
+ Author.where(['birthday <= ?', Time.now]).count.should == 3
37
+ end
38
+
39
+ it "rejects dup ID's" do
40
+ should.raise(ActiveRecord::RecordNotUnique){ Author.data(id: 3, name: "Three") }
41
+ end
42
+
43
+ it "loads a YAML file path/to/my.yml" do
44
+ Publisher.load_data
45
+ Publisher.count.should == 3
46
+ Publisher.find(2).name == 'Penguin'
47
+ end
48
+
49
+ it "supports reload!" do
50
+ Publisher.where('id is not null').delete_all # hackaround ReadOnlyRecord
51
+ Publisher.count.should == 0
52
+ Publisher.reload!
53
+ Publisher.count.should == 3
54
+ Publisher.data(id: 23, name: "Flop")
55
+ Publisher.count.should == 4
56
+ end
57
+
58
+ it "rejects missing data files" do
59
+ should.raise(Errno::ENOENT){ Publisher.load_data 'nope.yml' }
60
+ end
61
+
62
+ it "rejects empty data files" do
63
+ should.raise(ConstantRecord::BadDataFile){ Publisher.load_data 'spec/data/empty.yml' }
64
+ end
65
+ end
66
+
67
+ describe "creates constants" do
68
+ it "simple values" do
69
+ Publisher.data(id: 3, name: "Simple Value")
70
+ Publisher::SIMPLE_VALUE.should == 3
71
+ end
72
+ it "complex strings" do
73
+ Publisher.data(id: 4, name: " 2 Non-Fiction, Bestsellers! ")
74
+ Publisher::NON_FICTION_BESTSELLERS.should == 4
75
+ end
76
+ end
77
+
78
+ describe "associations" do
79
+ it "belongs_to" do
80
+ Article.create!(author_id: 1)
81
+ Article.create!(author_id: 2)
82
+ Article.create!(author_id: 3)
83
+ Article.create!(author_id: 2)
84
+ Article.create!(author_id: 1)
85
+ author = Author.find(1)
86
+ article = Article.find(5)
87
+ article.author.id.should == author.id
88
+ end
89
+
90
+ it "has_many" do
91
+ author = Author.find(2)
92
+ author.articles.count.should == 2
93
+ author.articles.find(4).id.should == 4
94
+ end
95
+
96
+ it "has_many through (up)" do
97
+ ArticlePublisher.create!(article_id: 4, publisher_id: 7)
98
+ ArticlePublisher.create!(article_id: 5, publisher_id: 7)
99
+ ArticlePublisher.create!(article_id: 60, publisher_id: 7) # bogus
100
+ publisher = Publisher.find(7)
101
+ publisher.name.should == "Marvel"
102
+ publisher.article_publishers.count.should == 3
103
+ publisher.articles.count.should == 2
104
+ publisher.articles.each do |art|
105
+ art.should == Article.find(art.id)
106
+ end
107
+ end
108
+
109
+ it "has_many through (down)" do
110
+ ArticlePublisher.create!(article_id: 2, publisher_id: 1)
111
+ ArticlePublisher.create!(article_id: 2, publisher_id: 2)
112
+ ArticlePublisher.create!(article_id: 2, publisher_id: 30) # bogus
113
+ article = Article.find(2)
114
+ article.article_publishers.count.should == 3
115
+ article.publishers.count.should == 2
116
+ end
117
+ end
118
+
119
+ describe "readonly records" do
120
+ before do
121
+ @publisher = Publisher.find(1)
122
+ end
123
+
124
+ it "readonly? == true" do
125
+ @publisher.readonly?.should.be.true?
126
+ end
127
+
128
+ it "rejects destroy" do
129
+ should.raise(ActiveRecord::ReadOnlyRecord){ @publisher.destroy }
130
+ end
131
+
132
+ it "rejects delete" do
133
+ should.raise(ActiveRecord::ReadOnlyRecord){ @publisher.delete }
134
+ end
135
+
136
+ it "rejects update_all" do
137
+ should.raise(ActiveRecord::ReadOnlyRecord){ Publisher.update_all('id = null') }
138
+ end
139
+
140
+ # it "rejects update_all thru associations" do
141
+ # should.raise(ActiveRecord::ReadOnlyRecord){ Publisher.where(id: 1).update_all('id = null') }
142
+ # end
143
+
144
+ it "rejects delete_all" do
145
+ should.raise(ActiveRecord::ReadOnlyRecord){ Publisher.delete_all }
146
+ end
147
+
148
+ # it "rejects delete_all thru associations" do
149
+ # should.raise(ActiveRecord::ReadOnlyRecord){ Publisher.where(id: 1).delete_all }
150
+ # end
151
+
152
+ it "rejects destroy_all" do
153
+ should.raise(ActiveRecord::ReadOnlyRecord){ Publisher.destroy_all }
154
+ end
155
+
156
+ # it "rejects destroy_all thru associations" do
157
+ # should.raise(ActiveRecord::ReadOnlyRecord){ Publisher.where(id: 1).destroy_all }
158
+ # end
159
+ end
160
+ end
161
+
@@ -0,0 +1,9 @@
1
+ ---
2
+ - id: 1
3
+ publisher_id: 1
4
+ name: Michael Pollan
5
+ - id: 2
6
+ name: Piers Anthony
7
+ - id: 3
8
+ publisher_id: 2
9
+ name: Chuck Palahniuk
File without changes
@@ -0,0 +1,7 @@
1
+ ---
2
+ - id: 1
3
+ name: Phaidon
4
+ - id: 2
5
+ name: Penguin
6
+ - id: 7
7
+ name: Marvel
@@ -0,0 +1,70 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+ require 'bacon'
3
+ Bacon.summary_at_exit
4
+ if $0 =~ /\brspec$/
5
+ raise "\n===\nThese tests are in bacon, not rspec. Try: bacon #{ARGV * ' '}\n===\n"
6
+ end
7
+
8
+ require 'date' # datetime
9
+ require 'constant_record'
10
+ require 'active_record'
11
+ require 'sqlite3'
12
+
13
+ # Override path for testing purposes
14
+ TEST_YAML_DATA_DIR = File.join(File.dirname(__FILE__), 'data')
15
+ ConstantRecord.data_dir = TEST_YAML_DATA_DIR
16
+
17
+ # Our "persistent" sqlite database for "real" records (not in-memory)
18
+ TEST_SQLITE_DB_FILE = File.join(File.dirname(__FILE__), 'test.sqlite3')
19
+ File.unlink TEST_SQLITE_DB_FILE rescue nil
20
+ at_exit do
21
+ File.unlink TEST_SQLITE_DB_FILE rescue nil
22
+ end
23
+
24
+ ActiveRecord::Base.establish_connection(
25
+ adapter: 'sqlite3',
26
+ database: TEST_SQLITE_DB_FILE,
27
+ pool: 5
28
+ )
29
+
30
+ if ENV['DEBUG']
31
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
32
+ ActiveRecord::Base.logger.level = Logger::DEBUG
33
+ end
34
+
35
+ class Author < ActiveRecord::Base
36
+ include ConstantRecord
37
+
38
+ has_many :articles
39
+ end
40
+
41
+ class Publisher < ActiveRecord::Base
42
+ include ConstantRecord
43
+
44
+ has_many :article_publishers
45
+ has_many :articles, through: :article_publishers
46
+ end
47
+
48
+ class Article < ActiveRecord::Base
49
+ include ConstantRecord::Associations
50
+ belongs_to :author
51
+ has_many :article_publishers
52
+ has_many :publishers, through: :article_publishers
53
+ end
54
+
55
+
56
+ class ArticlePublisher < ActiveRecord::Base
57
+ belongs_to :article
58
+ belongs_to :publisher
59
+ end
60
+
61
+ # Setup ActiveRecord tables
62
+ Article.connection.create_table(:articles) do |t|
63
+ t.string :title
64
+ t.integer :author_id
65
+ end
66
+
67
+ ArticlePublisher.connection.create_table(:article_publishers) do |t|
68
+ t.string :article_id
69
+ t.integer :publisher_id
70
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constant_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Nate Wiger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bacon
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: |2
84
+ In-memory ActiveRecord querying and associations for static data.
85
+ Improves performance and decreases bugs due to data mismatches.
86
+ email:
87
+ - nwiger@gmail.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".travis.yml"
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - constant_record.gemspec
99
+ - lib/constant_record.rb
100
+ - lib/constant_record/version.rb
101
+ - spec/constant_record_spec.rb
102
+ - spec/data/authors.yml
103
+ - spec/data/empty.yml
104
+ - spec/data/publishers.yml
105
+ - spec/spec_helper.rb
106
+ homepage: ''
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.2.2
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: In-memory ActiveRecord querying and associations for static data.
130
+ test_files:
131
+ - spec/constant_record_spec.rb
132
+ - spec/data/authors.yml
133
+ - spec/data/empty.yml
134
+ - spec/data/publishers.yml
135
+ - spec/spec_helper.rb