constant_record 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0c54e7e14d065796037c8c68789bf16ed980cbdc
4
- data.tar.gz: e2756dc08f225ce136ac3f07dc20d1b773fa579e
3
+ metadata.gz: ddf57212499798179e38bd340e90f0ae680629ac
4
+ data.tar.gz: 9ae1ca8ef06c517f9a834c6c29e5ccfe4f53e95d
5
5
  SHA512:
6
- metadata.gz: 56847eabe09d8c35111905feca49d35e8dafc5ea70226b213e05c116ea01b75d61709aa6e553e3aa5902a22e7a910232082cf3c7663cd1f2605b64f4f254059d
7
- data.tar.gz: d5c59bf34e24b0ab5b7fbfcd509de8c5e006cf164528dada95de3a6183486f412c40b4214a1eafcd5be97676c0975f6a9f9030ea2d776070bac7b0cada8bb6e8
6
+ metadata.gz: cce30cf09c75bf5a30b13f2a38e25ac4ae2dcfbe14d31b76e055d34f9f63f6c4fe6f3cc873a2df006da63a902155bdfee3e35687031de38ec2595b39c9ff263f
7
+ data.tar.gz: 95ae356510c8d29ae59bf5e1551e71e1f1dbe72c8894f680c64c7303290a71acf281a73fe99414f7e12736fa89fe93f8ae50f6362647ec05ba46922c2006872f
data/README.md CHANGED
@@ -8,7 +8,7 @@ Compatible with all current versions of Rails, from 3.x up through 4.1
8
8
  Unlike previous (ambitious) approaches that have tried to duplicate ActiveRecord
9
9
  functionality in a separate set of classes, this is a simple shim of < 200 LOC
10
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
11
+ to minimize breakage between Rails versions, and also avoids reimplementing ActiveRecord
12
12
  features.
13
13
 
14
14
  ## Installation
@@ -25,12 +25,10 @@ Then run `bundle install`. Or, install it yourself manually, if you're into that
25
25
 
26
26
  ## Usage
27
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
28
+ Inherit from `ConstantRecord::Base` just like you would with ActiveRecord.
29
+ Then, you can use `data` to add data directly in that class for clarity:
33
30
 
31
+ class Genre < ConstantRecord::Base
34
32
  data id: 1, name: "Rock", slug: "rock"
35
33
  data id: 2, name: "Hip-Hop", slug: "hiphop"
36
34
  data id: 3, name: "Pop", slug: "pop"
@@ -38,8 +36,7 @@ data directly in that class for clarity:
38
36
 
39
37
  Or, you can choose to keep your data in a YAML file:
40
38
 
41
- class Genre < ActiveRecord::Base
42
- include ConstantRecord
39
+ class Genre < ConstantRecord::Base
43
40
  load_data File.join(Rails.root, 'config', 'data', 'genres.yml')
44
41
  end
45
42
 
@@ -59,8 +56,7 @@ The YAML file should be an array of hashes:
59
56
 
60
57
  You can omit the filename if it follows the naming convention of `config/data/[table_name].yml`:
61
58
 
62
- class Genre < ActiveRecord::Base
63
- include ConstantRecord
59
+ class Genre < ConstantRecord::Base
64
60
  load_data # config/data/genres.yml
65
61
  end
66
62
 
@@ -76,9 +72,7 @@ you use in the *first* `data` declaration. **Important:** This means if you hav
76
72
  columns that aren't always present, *make sure to include them with `column_name: nil` on
77
73
  the first `data` line:*
78
74
 
79
- class Genre < ActiveRecord::Base
80
- include ConstantRecord
81
-
75
+ class Genre < ConstantRecord::Base
82
76
  data id: 1, name: "Rock", slug: "rock", region: nil, country: nil
83
77
  data id: 2, name: "Hip-Hop", slug: "hiphop", region: 'North America'
84
78
  data id: 3, name: "Pop", slug: "pop", country: 'US'
@@ -98,47 +92,19 @@ Attempts to modify values will fail:
98
92
 
99
93
  You'll get an `ActiveRecord::ReadOnlyRecord` exception.
100
94
 
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
95
  ## Associations
128
96
 
129
97
  Internally, ActiveRecord tries to do joins to retrieve associations. This doesn't work, since
130
98
  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
99
+ in the ActiveRecord class that is trying to associate to your ConstantRecord class:
135
100
 
101
+ class Genre < ConstantRecord::Base
136
102
  has_many :song_genres
137
103
  has_many :songs, through: :song_genres
138
104
 
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'
105
+ data id: 1, name: "Rock", slug: "rock"
106
+ data id: 2, name: "Hip-Hop", slug: "hiphop"
107
+ data id: 3, name: "Pop", slug: "pop"
142
108
  end
143
109
 
144
110
  class SongGenre < ActiveRecord::Base
@@ -147,7 +113,7 @@ in the normal ActiveRecord class that is trying to associate to your ConstantRec
147
113
  end
148
114
 
149
115
  class Song < ActiveRecord::Base
150
- include ConstantRecord::Associations
116
+ include ConstantRecord::Associations # <-- IMPORTANT
151
117
  has_many :song_genres
152
118
  has_many :songs, through: :song_genres
153
119
  end
@@ -161,12 +127,35 @@ If you forget to do this, you'll get an error like this:
161
127
  It would be great to remove this shim, but I can't currently see a way without monkey-patching
162
128
  the internals of ActiveRecord, which I don't want to do for 17 different reasons.
163
129
 
130
+ ## Auto Constants
131
+
132
+ ConstantRecord will also create constants on the fly for you if you have a `name` column.
133
+ Revisiting our example:
134
+
135
+ class Genre < ConstantRecord::Base
136
+ data id: 1, name: "Rock", slug: "rock"
137
+ data id: 2, name: "Hip-Hop", slug: "hiphop"
138
+ data id: 3, name: "Pop", slug: "pop"
139
+ end
140
+
141
+ This will create:
142
+
143
+ Genre::ROCK = 1
144
+ Genre::HIP_HOP = 2
145
+ Genre::POP = 3
146
+
147
+ This makes it cleaner to do queries in your app:
148
+
149
+ Genre.find(Genre::ROCK)
150
+ Song.where(genre_id: Genre::ROCK)
151
+
152
+ And so on.
153
+
164
154
  ## Debugging
165
155
 
166
156
  If you forget to define data, you'll get a "table doesn't exist" error:
167
157
 
168
- class Publisher < ActiveRecord::Base
169
- include ConstantRecord
158
+ class Publisher < ConstantRecord::Base
170
159
 
171
160
  # Oops no data
172
161
 
@@ -182,9 +171,7 @@ This is because the table is created lazily when you first load data.
182
171
 
183
172
  If you try to add a custom column on a different `data` line:
184
173
 
185
- class Genre < ActiveRecord::Base
186
- include ConstantRecord
187
-
174
+ class Genre < ConstantRecord::Base
188
175
  data id: 1, name: "Rock", slug: "rock"
189
176
  data id: 2, name: "Hip-Hop", slug: "hiphop"
190
177
  data id: 3, name: "Pop", slug: "pop", ranking: 1 # oops
@@ -1,3 +1,3 @@
1
1
  module ConstantRecord
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -1,10 +1,8 @@
1
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
2
+ # Inherit from `ConstantRecord::Base` just like you would with ActiveRecord.
3
+ # Then, you can use `data` to add data directly in that class for clarity:
7
4
  #
5
+ # class Genre < ConstantRecord::Base
8
6
  # data id: 1, name: "Rock", slug: "rock"
9
7
  # data id: 2, name: "Hip-Hop", slug: "hiphop"
10
8
  # data id: 3, name: "Pop", slug: "pop"
@@ -12,8 +10,7 @@
12
10
  #
13
11
  # Or, you can choose to keep your data in a YAML file:
14
12
  #
15
- # class Genre < ActiveRecord::Base
16
- # include ConstantRecord
13
+ # class Genre < ConstantRecord::Base
17
14
  # load_data File.join(Rails.root, 'config', 'data', 'genres.yml')
18
15
  # end
19
16
  #
@@ -27,18 +24,9 @@ module ConstantRecord
27
24
  class Error < StandardError; end
28
25
  class BadDataFile < Error; end
29
26
 
30
- MEMORY_DBCONFIG = {adapter: 'sqlite3', database: ":memory:", pool: 5}.freeze
31
-
27
+ DATABASE_CONFIG = { adapter: 'sqlite3', database: ":memory:", pool: 5 }.freeze
32
28
 
33
29
  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
30
  def data_dir
43
31
  @data_dir || File.join('config', 'data')
44
32
  end
@@ -46,13 +34,6 @@ module ConstantRecord
46
34
  def data_dir=(path)
47
35
  @data_dir = path
48
36
  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
37
  end
57
38
 
58
39
  #
@@ -77,6 +58,7 @@ module ConstantRecord
77
58
  end
78
59
 
79
60
  # Call our method to populate data
61
+ @data_rows = []
80
62
  records.each{|r| data r}
81
63
 
82
64
  @loaded = true
@@ -91,30 +73,35 @@ module ConstantRecord
91
73
  end
92
74
 
93
75
  # Define a constant record: data id: 1, name: "California", slug: "CA"
94
- def data(attrib)
95
- attrib.symbolize_keys!
76
+ def data(attrib, reload=false)
96
77
  raise ArgumentError, "#{self}.data expects a Hash of attributes" unless attrib.is_a?(Hash)
78
+ attrib.symbolize_keys!
97
79
 
98
80
  unless attrib[primary_key.to_sym]
99
81
  raise ArgumentError, "#{self}.data missing primary key '#{primary_key}': #{attrib.inspect}"
100
82
  end
101
83
 
102
- unless @table_was_created
103
- create_memory_table(attrib)
104
- @table_was_created = true
84
+ # Save data definitions for reload on connection change
85
+ @data_rows ||= []
86
+
87
+ # Check for duplicates
88
+ unless reload
89
+ if old_record = @data_rows.detect{|r| r[primary_key.to_sym] == attrib[primary_key.to_sym] }
90
+ raise ActiveRecord::RecordNotUnique,
91
+ "Duplicate #{self} id=#{attrib[primary_key.to_sym]} found: #{attrib} vs #{old_record}"
92
+ end
93
+ @data_rows << attrib
105
94
  end
106
95
 
96
+ # Create table dynamically based on first row of data
97
+ create_memory_table(attrib) unless connection.table_exists?(table_name)
98
+
99
+ # Save to in-memory table
107
100
  new_record = new(attrib)
108
101
  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
102
  new_record.save!
116
103
 
117
- # create Ruby constants as well, so "id: 3, name: Sky" gets SKY=3
104
+ # Create Ruby constants as well, so "id: 3, name: Sky" generates SKY=3
118
105
  if new_record.respond_to?(:name) and name = new_record.name
119
106
  const_name =
120
107
  name.to_s.upcase.strip.gsub(/[-\s]+/,'_').sub(/^[0-9_]+/,'').gsub(/\W+/,'')
@@ -146,6 +133,12 @@ module ConstantRecord
146
133
  end
147
134
  end
148
135
  end
136
+
137
+ # Reloads the table when the connection has changed
138
+ def reload_memory_table
139
+ return false unless @data_rows
140
+ @data_rows.each{|r| data r, true}
141
+ end
149
142
  end
150
143
 
151
144
  #
@@ -153,7 +146,7 @@ module ConstantRecord
153
146
  #
154
147
  module Associations
155
148
  def self.included(base)
156
- base.extend self # support "include" as well
149
+ base.extend self # support "include"
157
150
  end
158
151
 
159
152
  #
@@ -163,7 +156,9 @@ module ConstantRecord
163
156
  # so that we're not making real DB calls.
164
157
  #
165
158
  def has_many(other_table, options={})
166
- # puts "#{self}(#{table_name}).has_many #{other_table.inspect}, #{options.inspect}"
159
+ super other_table, options.dup # make AR happy
160
+
161
+ # Redefine association method in the class
167
162
  if join_tab = options[:through]
168
163
  foreign_key = options[:foreign_key] || other_table.to_s.singularize.foreign_key
169
164
  prime_key = options[:primary_key] || primary_key
@@ -176,8 +171,6 @@ module ConstantRecord
176
171
  return [] if ids.empty?
177
172
  class_name.constantize.where(id: ids)
178
173
  end
179
- else
180
- super other_table, options
181
174
  end
182
175
  end
183
176
  end
@@ -217,4 +210,27 @@ module ConstantRecord
217
210
  end
218
211
  end
219
212
  end
213
+
214
+ #
215
+ # Base class to inherit from so we can share the same memory database
216
+ #
217
+ class Base < ActiveRecord::Base
218
+ extend DataLoading
219
+ extend Associations
220
+ include ReadOnly
221
+
222
+ # Reload table if connection changes. Since it's in-memory, a connection
223
+ # change means the the table gets wiped.
224
+ def self.connection
225
+ conn = super
226
+ if (@previous_connection ||= conn) != conn
227
+ @previous_connection = conn # avoid infinite loop
228
+ reload_memory_table
229
+ end
230
+ conn
231
+ end
232
+
233
+ self.abstract_class = true
234
+ establish_connection ConstantRecord::DATABASE_CONFIG
235
+ end
220
236
  end
@@ -1,6 +1,30 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
3
  describe "ConstantRecord" do
4
+ describe "connection" do
5
+ it "has an in-memory DB" do
6
+ dbconfig = Author.connection.instance_variable_get(:@config)
7
+ dbconfig[:adapter].should == "sqlite3"
8
+ dbconfig[:database].should == ":memory:"
9
+ end
10
+
11
+ it "shares the same database" do
12
+ Author.connection.should == Publisher.connection
13
+ Author.connection.should == ConstantRecord::Base.connection
14
+ end
15
+
16
+ it "reloads the table on connection change" do
17
+ Publisher.load_data
18
+ old_con = Publisher.connection
19
+ pub_ids = Publisher.pluck(:id)
20
+ ConstantRecord::Base.remove_connection
21
+ new_con = Publisher.connection
22
+ new_con.should != old_con
23
+ Publisher.pluck(:id).should == pub_ids
24
+ Publisher.where('id is not null').delete_all # hackaround ReadOnlyRecord
25
+ end
26
+ end
27
+
4
28
  describe "loading data" do
5
29
  it "data(attr: val)" do
6
30
  date1 = Time.now
@@ -93,7 +117,7 @@ describe "ConstantRecord" do
93
117
  author.articles.find(4).id.should == 4
94
118
  end
95
119
 
96
- it "has_many through (up)" do
120
+ it "has_many through (down)" do
97
121
  ArticlePublisher.create!(article_id: 4, publisher_id: 7)
98
122
  ArticlePublisher.create!(article_id: 5, publisher_id: 7)
99
123
  ArticlePublisher.create!(article_id: 60, publisher_id: 7) # bogus
@@ -106,13 +130,15 @@ describe "ConstantRecord" do
106
130
  end
107
131
  end
108
132
 
109
- it "has_many through (down)" do
133
+ it "has_many through (up)" do
110
134
  ArticlePublisher.create!(article_id: 2, publisher_id: 1)
111
135
  ArticlePublisher.create!(article_id: 2, publisher_id: 2)
112
136
  ArticlePublisher.create!(article_id: 2, publisher_id: 30) # bogus
113
137
  article = Article.find(2)
114
138
  article.article_publishers.count.should == 3
115
139
  article.publishers.count.should == 2
140
+ publisher = article.publishers.find(1)
141
+ publisher.should == Publisher.find(1)
116
142
  end
117
143
  end
118
144
 
data/spec/spec_helper.rb CHANGED
@@ -32,15 +32,11 @@ if ENV['DEBUG']
32
32
  ActiveRecord::Base.logger.level = Logger::DEBUG
33
33
  end
34
34
 
35
- class Author < ActiveRecord::Base
36
- include ConstantRecord
37
-
35
+ class Author < ConstantRecord::Base
38
36
  has_many :articles
39
37
  end
40
38
 
41
- class Publisher < ActiveRecord::Base
42
- include ConstantRecord
43
-
39
+ class Publisher < ConstantRecord::Base
44
40
  has_many :article_publishers
45
41
  has_many :articles, through: :article_publishers
46
42
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: constant_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Wiger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-18 00:00:00.000000000 Z
11
+ date: 2014-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord