offline_lookup 0.0.3 → 1.0.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 +98 -14
- data/lib/offline_lookup.rb +4 -110
- data/lib/offline_lookup/active_record.rb +42 -0
- data/lib/offline_lookup/base.rb +54 -0
- data/lib/offline_lookup/builder.rb +48 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a980e6edb149fd22cc7c81f2986ddac4e6ca27dc
|
4
|
+
data.tar.gz: dedadea949a7706a88188eb2bd0a03b31530ac34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bd3263c95eb71ea8a2a5f511ee075a8a893eaf56f9aedf5c40b84c5addd49ad1230e2506270f407010c719391602e713e0ec8ea641585f3af134df3ecb63a9d
|
7
|
+
data.tar.gz: cdefda2de296628c482b77bd156b08213a1205d696674682524ba6e99cc3ceb1be595d50e1d280ad6fae148146e76ed7f91b1e20c6b2a80511454aadace64ac4
|
data/README.md
CHANGED
@@ -1,18 +1,71 @@
|
|
1
|
-
|
1
|
+
#Offline Lookup
|
2
2
|
|
3
|
-
|
3
|
+
Store lookup values for small tables for fewer db queries and syntactic sugar.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
## The quickest examples:
|
6
|
+
Lets say you have a model `PostType` with a few rows that define various types of Posts.
|
7
|
+
|
8
|
+
`PostType.find_by(name: "reply").id`
|
9
|
+
becomes simply
|
10
|
+
`Post.reply_id`
|
11
|
+
|
12
|
+
Where the latter form does not execute a database query. Instead, a mapping of id to name is kept in the class instance. Thus, this is good only for small lookup-type tables, not for full tables of data.
|
13
|
+
|
14
|
+
A couple more:
|
15
|
+
`PostType.find_by(name: name)` becomes `PostType.lookup(name)`
|
16
|
+
`post_type.name == "reply"` becomes `post_type.reply?` (disabled by default, use `identity_methods: true`)
|
17
|
+
`Post.find_by(name: "reply") becomes `PostType.reply` (disabled by default, use `lookup_methods: true`)
|
18
|
+
|
19
|
+
More flexible, still no db queries:
|
20
|
+
`PostType.find_by(name: name).id` becomes `PostType.id_for_name(name)`
|
21
|
+
`PostType.find(1).name` becomes `PostType.name_for_id(1)`
|
22
|
+
|
23
|
+
IMPORTANT: don't use this for models that have a lot of data!! While OfflineLookup only stores primary key and lookup column values, you don't usually want this loading thousands or millions of such values into memory. Rule of thumb, keep it in the tens or below.
|
24
|
+
|
25
|
+
### What's New
|
26
|
+
|
27
|
+
v1.0.0
|
28
|
+
`lookup_methods` and `identity_methods` now default to false. If you use the `TurnaroundLevel.two_hour` or `TurnaroundLevel.first.two_hour?` methods, pass either `lookup_methods: true` or `identity_methods: true` into `use_offline_lookup`.
|
29
|
+
|
30
|
+
Add `lookup` method to allow lookup by key'd name without risking bad / reserved-keyword method names (e.g. "parent")
|
31
|
+
|
32
|
+
You can now specify multiple columns for lookup! The values are by default joined with a " " (note this translates to "_" for method names). You can configure this delimiter and what to do with `nil` values.
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
## How To Use It
|
37
|
+
|
38
|
+
### By Example
|
9
39
|
|
10
40
|
```
|
41
|
+
class TurnaroundLevel
|
42
|
+
use_offline_lookup :name
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return the id of the TurnaroundLevel named "Two Hour"
|
11
46
|
TurnaroundLevel.two_hour_id
|
12
|
-
TurnaroundLevel.
|
13
|
-
|
47
|
+
TurnaroundLevel.id_for_name("Two Hour")
|
48
|
+
# Return the instance of TurnaroundLevel with the name "Two Hour"
|
49
|
+
TurnaroundLevel.lookup("Two Hour")
|
50
|
+
#Return the name of the TurnaroundLevel with id 7
|
51
|
+
TurnaroundLevel.name_for_id(7)
|
52
|
+
```
|
53
|
+
|
54
|
+
A few extra options:
|
55
|
+
```
|
56
|
+
class TurnaroundLevel
|
57
|
+
use_offline_lookup :name, lookup_method: true, identity_method: true
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return true if the last TurnaroundLevel is the "Two Hour" level. Uses the `:identify_methods` options.
|
61
|
+
TurnaroundLevel.last.two_hour?
|
62
|
+
# Return the "Two Hour" TurnaroundLevel instance. Uses the `:lookup_methods` option
|
63
|
+
TurnaroundLevel.two_hour
|
64
|
+
|
14
65
|
```
|
15
66
|
|
67
|
+
### By Spec
|
68
|
+
|
16
69
|
In any ActiveRecord::Base subclass, use:
|
17
70
|
|
18
71
|
`use_offline_id_lookup column_name`
|
@@ -40,22 +93,53 @@ TurnaroundLevel.name_for_id(1)
|
|
40
93
|
#=> 'Same Day'
|
41
94
|
TurnaroundLevel.id_for_name('Same Day')
|
42
95
|
#=> 1
|
96
|
+
TurnaroundLevel.lookup(`Same Day`)
|
97
|
+
#=> <#TurnaroundLevel id: 1, level: "Same Day", ...>
|
98
|
+
```
|
99
|
+
|
100
|
+
If you use the option `identity_methods: true`, you get
|
101
|
+
|
102
|
+
```
|
43
103
|
TurnaroundLevel.first.same_day?
|
44
104
|
#=> true
|
105
|
+
```
|
106
|
+
|
107
|
+
Using `lookup_methods: true`:
|
108
|
+
|
109
|
+
```
|
45
110
|
TurnaroundLevel.same_day
|
46
111
|
#=> <#TurnaroundLevel id: 1, level: "Same Day", ...>
|
47
112
|
```
|
48
113
|
|
49
|
-
|
114
|
+
This is not quite offline. This is because we only store the key - name mappings, not the entire objects, in memory when we declare a new offline_lookup model. However it is included by default for convenient syntax (and it uses a lookup on what is usually the primary key of the table, in case the extra few ms matter to you).
|
115
|
+
|
116
|
+
|
117
|
+
You can use combinations of columns to define the lookup values
|
50
118
|
|
51
|
-
|
119
|
+
```
|
120
|
+
class Admin < ActiveRecord::Base # firstname, lastname
|
121
|
+
use_offline_lookup :firstname, :lastname
|
122
|
+
end
|
52
123
|
|
53
|
-
|
124
|
+
Admin.john_doe_id
|
125
|
+
Admin.lookup("John Doe")
|
126
|
+
```
|
54
127
|
|
55
|
-
|
128
|
+
Option include
|
129
|
+
`delimiter` (default: `'_'`): character or string to join values between columns
|
130
|
+
`compact` (default: `false`): exclude nil columns from joining with delimiter
|
131
|
+
`name` (default: fields.join(delimiter)): name for this lookup
|
56
132
|
|
57
|
-
|
133
|
+
E.g. on `name`:
|
134
|
+
```
|
135
|
+
class Admin < ActiveRecord::Base # firstname, lastname
|
136
|
+
use_offline_lookup :firstname, :lastname, name: "name"
|
137
|
+
end
|
138
|
+
Admin.id_for_name("John Doe")
|
139
|
+
```
|
58
140
|
|
59
141
|
## Known Issues
|
60
142
|
|
61
|
-
If two entries in the table have the same value in the specified field, all but one will get overwritten.
|
143
|
+
If two entries in the table have the same value in the specified field, all but one will get overwritten.
|
144
|
+
|
145
|
+
Be aware that if the lookup name is a keyword or existing method, this can cause issues! For example, I encountered a use of offline lookup where the lookup column was `lastname` of a small `Admin` table. One of the last names was "parent". With `identity_methods: true`, this defined the `Admin.parent` method, which of course caused issues since `parent` on a class is supposed to mean something else! If you have potentially dangerous values, leave lookup_methods disabled and just use `Admin.lookup("Parent")` instead.
|
data/lib/offline_lookup.rb
CHANGED
@@ -1,114 +1,8 @@
|
|
1
|
-
# TODO: provide multiple column names in a single call (e.g. firstname, lastname)
|
2
1
|
# TODO: support multiple offline lookups per model
|
3
2
|
# TODO: support scope arg in use_offline_lookup (partial index)
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
class_attribute :offline_lookup_values, :offline_lookup_options
|
9
|
-
self.offline_lookup_options = {
|
10
|
-
field: field.to_s,
|
11
|
-
key: key.to_s,
|
12
|
-
identity_methods: !!identity_methods,
|
13
|
-
lookup_methods: !!lookup_methods
|
14
|
-
}.freeze
|
15
|
-
self.offline_lookup_values = self.all.pluck(key, field).to_h.freeze
|
4
|
+
require 'offline_lookup/active_record.rb'
|
5
|
+
require 'offline_lookup/base.rb'
|
6
|
+
require 'offline_lookup/builder.rb'
|
16
7
|
|
17
|
-
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class Builder
|
22
|
-
def initialize(options)
|
23
|
-
@field = options[:field]
|
24
|
-
@key = options[:key]
|
25
|
-
end
|
26
|
-
|
27
|
-
def sanitize(string)
|
28
|
-
#:methodize went away. Where did it go?
|
29
|
-
#1. Replace illegal chars and _ boundaries with " " boundary
|
30
|
-
string = string.gsub(/[^a-zA-Z\d]+/," ").strip
|
31
|
-
#2. Insert " " boundary at snake-case boundaries
|
32
|
-
string.gsub!(/([a-z])([A-Z])/){|s| "#{$1} #{$2}"}
|
33
|
-
#3. underscore
|
34
|
-
string.gsub!(/\s+/, "_")
|
35
|
-
string.downcase!
|
36
|
-
#4. Append underscore if name begins with digit
|
37
|
-
string = "_#{string}" if string.length == 0 || string[0] =~ /\d/
|
38
|
-
return string
|
39
|
-
end
|
40
|
-
|
41
|
-
# e.g., :two_hour_id
|
42
|
-
def key_method_name(value)
|
43
|
-
sanitize "#{value}_#{@key}"
|
44
|
-
end
|
45
|
-
|
46
|
-
def lookup_method_name(value)
|
47
|
-
sanitize value.to_s
|
48
|
-
end
|
49
|
-
|
50
|
-
# e.g., :two_hour?
|
51
|
-
def indentiy_method_name(value)
|
52
|
-
lookup_method_name(value) + "?"
|
53
|
-
end
|
54
|
-
|
55
|
-
# e.g. :name_for_id(id)
|
56
|
-
def field_for_key_method_name
|
57
|
-
sanitize "#{@field}_for_#{@key}"
|
58
|
-
end
|
59
|
-
|
60
|
-
# e.g. :id_for_name(name)
|
61
|
-
def key_for_field_method_name
|
62
|
-
sanitize "#{@key}_for_#{@field}"
|
63
|
-
end
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
module Base
|
68
|
-
extend ActiveSupport::Concern
|
69
|
-
|
70
|
-
included do
|
71
|
-
builder = OfflineLookup::Builder.new(self.offline_lookup_options)
|
72
|
-
|
73
|
-
### define value-named methods such as :two_hour_id and :two_hour?
|
74
|
-
|
75
|
-
self.offline_lookup_values.each do |key, value|
|
76
|
-
# class method: get key value (e.g. FooType.bar_id)
|
77
|
-
define_singleton_method(builder.key_method_name(value)) do
|
78
|
-
key
|
79
|
-
end
|
80
|
-
|
81
|
-
# instance method: true if instance is of named type (e.g. FooType.first.bar?)
|
82
|
-
if self.offline_lookup_options[:identity_methods]
|
83
|
-
define_method(builder.indentiy_method_name(value)) do
|
84
|
-
self.attributes[self.offline_lookup_options[:key]] == key
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
# class method: get instance by named method (e.g. FooType.bar)
|
89
|
-
# not "Offline", but lookup by indexed key. Also, synactic sugar.
|
90
|
-
if self.offline_lookup_options[:lookup_methods]
|
91
|
-
define_singleton_method(builder.lookup_method_name(value)) do
|
92
|
-
key = self.offline_lookup_values.find{|k, v| v.to_s == value.to_s}.first
|
93
|
-
find(key)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
|
99
|
-
### define statically-named methods where you pass in the named value, e.g., id_for_name(:two_hour)
|
100
|
-
# e.g. FooType.name_for_id(1)
|
101
|
-
define_singleton_method(builder.field_for_key_method_name) do |key_value|
|
102
|
-
self.offline_lookup_values[key_value]
|
103
|
-
end
|
104
|
-
|
105
|
-
# e.g. FooType.id_for_name("Bar")
|
106
|
-
define_singleton_method(builder.key_for_field_method_name) do |field_value|
|
107
|
-
self.offline_lookup_values.find{|k, v| v.to_s == field_value.to_s}.first
|
108
|
-
end
|
109
|
-
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
ActiveRecord::Base.extend OfflineLookup::ActiveRecord
|
8
|
+
ActiveRecord::Base.include OfflineLookup::ActiveRecord
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module OfflineLookup
|
2
|
+
module ActiveRecord
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
after_create :get_offline_lookup_values
|
7
|
+
after_destroy :get_offline_lookup_values
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_offline_lookup_values
|
11
|
+
self.class.get_offline_lookup_values
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def use_offline_lookup(*fields, key: "id", identity_methods: false, lookup_methods: false, compact: false, delimiter: " ", name: fields.join(delimiter), transform: nil)
|
16
|
+
class_attribute :offline_lookup_values, :offline_lookup_options
|
17
|
+
self.offline_lookup_options = {
|
18
|
+
fields: fields.map(&:to_s),
|
19
|
+
key: key.to_s,
|
20
|
+
identity_methods: !!identity_methods,
|
21
|
+
lookup_methods: !!lookup_methods,
|
22
|
+
compact: !!compact,
|
23
|
+
delimiter: delimiter.to_s,
|
24
|
+
name: name,
|
25
|
+
transform: transform
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
get_offline_lookup_values
|
29
|
+
|
30
|
+
include OfflineLookup::Base
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_offline_lookup_values
|
34
|
+
self.offline_lookup_values = self.all.pluck(offline_lookup_options[:key], *offline_lookup_options[:fields]).map do |key, *fields|
|
35
|
+
fields.compact! if offline_lookup_options[:compact]
|
36
|
+
value = offline_lookup_options[:transform].present? ? offline_lookup_options[:transform].call(*fields.map(&:to_s)) : fields.map(&:to_s).join(offline_lookup_options[:delimiter])
|
37
|
+
[key, value]
|
38
|
+
end.to_h.freeze
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module OfflineLookup
|
2
|
+
module Base
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
builder = OfflineLookup::Builder.new(self.offline_lookup_options)
|
7
|
+
|
8
|
+
### define value-named methods such as :two_hour_id and :two_hour?
|
9
|
+
|
10
|
+
self.offline_lookup_values.each do |key, value|
|
11
|
+
# class method: get key value (e.g. FooType.bar_id)
|
12
|
+
define_singleton_method(builder.key_method_name(value)) do
|
13
|
+
key
|
14
|
+
end
|
15
|
+
|
16
|
+
# instance method: true if instance is of named type (e.g. FooType.first.bar?)
|
17
|
+
if self.offline_lookup_options[:identity_methods]
|
18
|
+
define_method(builder.indentiy_method_name(value)) do
|
19
|
+
self.attributes[self.offline_lookup_options[:key]] == key
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# class method: get instance by named method (e.g. FooType.bar)
|
24
|
+
# not "Offline", but lookup by indexed key. Also, synactic sugar.
|
25
|
+
if self.offline_lookup_options[:lookup_methods]
|
26
|
+
define_singleton_method(builder.lookup_method_name(value)) do
|
27
|
+
key = self.offline_lookup_values.find{|k, v| v.to_s == value.to_s}.try(:first)
|
28
|
+
find(key)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# class method: get instance using more general `lookup` method
|
33
|
+
# Just as not "offline" as above, but less dangerous / more robust to any db value
|
34
|
+
define_singleton_method :lookup do |value|
|
35
|
+
key = self.offline_lookup_values.find{|k, v| v.to_s == value.to_s}.try(:first)
|
36
|
+
find_by(id: key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
### define statically-named methods where you pass in the named value, e.g., id_for_name(:two_hour)
|
42
|
+
# e.g. FooType.name_for_id(1)
|
43
|
+
define_singleton_method(builder.field_for_key_method_name) do |key_value|
|
44
|
+
self.offline_lookup_values[key_value]
|
45
|
+
end
|
46
|
+
|
47
|
+
# e.g. FooType.id_for_name("Bar")
|
48
|
+
define_singleton_method(builder.key_for_field_method_name) do |field_value|
|
49
|
+
self.offline_lookup_values.find{|k, v| v.to_s == field_value.to_s}.try(:first)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module OfflineLookup
|
2
|
+
class Builder
|
3
|
+
def initialize(options)
|
4
|
+
@fields = options[:fields]
|
5
|
+
@key = options[:key]
|
6
|
+
@name = options[:name]
|
7
|
+
end
|
8
|
+
|
9
|
+
def sanitize(string)
|
10
|
+
#:methodize went away. Where did it go?
|
11
|
+
#1. Replace illegal chars and _ boundaries with " " boundary
|
12
|
+
string = string.gsub(/[^a-zA-Z\d]+/," ").strip
|
13
|
+
#2. Insert " " boundary at snake-case boundaries
|
14
|
+
string.gsub!(/([a-z])([A-Z])/){|s| "#{$1} #{$2}"}
|
15
|
+
#3. underscore
|
16
|
+
string.gsub!(/\s+/, "_")
|
17
|
+
string.downcase!
|
18
|
+
#4. Append underscore if name begins with digit
|
19
|
+
string = "_#{string}" if string.length == 0 || string[0] =~ /\d/
|
20
|
+
return string
|
21
|
+
end
|
22
|
+
|
23
|
+
# e.g., :two_hour_id
|
24
|
+
def key_method_name(value)
|
25
|
+
sanitize "#{value}_#{@key}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def lookup_method_name(value)
|
29
|
+
sanitize value.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
# e.g., :two_hour?
|
33
|
+
def indentiy_method_name(value)
|
34
|
+
lookup_method_name(value) + "?"
|
35
|
+
end
|
36
|
+
|
37
|
+
# e.g. :name_for_id(id)
|
38
|
+
def field_for_key_method_name
|
39
|
+
sanitize "#{@name}_for_#{@key}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# e.g. :id_for_name(name)
|
43
|
+
def key_for_field_method_name
|
44
|
+
sanitize "#{@key}_for_#{@name}"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: offline_lookup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Schwartz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-19 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description: Offline indexing of small tables
|
13
|
+
description: Offline indexing of small tables syntactic sugar & less db touching
|
14
14
|
email: ozydingo@gmail.com
|
15
15
|
executables: []
|
16
16
|
extensions: []
|
@@ -18,6 +18,9 @@ extra_rdoc_files: []
|
|
18
18
|
files:
|
19
19
|
- README.md
|
20
20
|
- lib/offline_lookup.rb
|
21
|
+
- lib/offline_lookup/active_record.rb
|
22
|
+
- lib/offline_lookup/base.rb
|
23
|
+
- lib/offline_lookup/builder.rb
|
21
24
|
homepage:
|
22
25
|
licenses:
|
23
26
|
- MIT
|