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 +4 -4
- data/README.md +39 -52
- data/lib/constant_record/version.rb +1 -1
- data/lib/constant_record.rb +56 -40
- data/spec/constant_record_spec.rb +28 -2
- data/spec/spec_helper.rb +2 -6
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddf57212499798179e38bd340e90f0ae680629ac
|
4
|
+
data.tar.gz: 9ae1ca8ef06c517f9a834c6c29e5ccfe4f53e95d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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 <
|
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 <
|
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 <
|
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
|
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"
|
140
|
-
data id: 2, name: "Hip-Hop", slug: "hiphop"
|
141
|
-
data id: 3, name: "Pop", slug: "pop"
|
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 <
|
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 <
|
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
|
data/lib/constant_record.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
#
|
2
|
-
#
|
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 <
|
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
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|
-
#
|
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"
|
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
|
-
|
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 (
|
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 (
|
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 <
|
36
|
-
include ConstantRecord
|
37
|
-
|
35
|
+
class Author < ConstantRecord::Base
|
38
36
|
has_many :articles
|
39
37
|
end
|
40
38
|
|
41
|
-
class Publisher <
|
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.
|
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-
|
11
|
+
date: 2014-06-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|