constant_record 0.5.0 → 0.6.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 +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
|