pluck_map 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +111 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/pluck_map/attribute.rb +54 -0
- data/lib/pluck_map/presenter.rb +89 -0
- data/lib/pluck_map/version.rb +3 -0
- data/lib/pluck_map.rb +5 -0
- data/pluck_map.gemspec +22 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 77e7669a17721684395128655dda6e8da49d30b1
|
4
|
+
data.tar.gz: 355d90c4ed3e0dfe1667c1666a9c32c7ea573d42
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 05ed5427a51748878512c6347f111890365264c011e192eea8e59fd2474dc175f935ee6434b98786603c16641a8bf4a7c6c5e9c5c3a77c203c0a1f902eb10b29
|
7
|
+
data.tar.gz: 5d3f898e451f9aea4ab28d237e86a38e7edf3ae353e00672a89451f699bd222e4456e187889e180eb33640e6846d92a69410d9c52c37cf31b3750ab0ebdb6bc2
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# PluckMap::Presenter
|
2
|
+
|
3
|
+
The PluckMap presenter provides a DSL for creating performant presenters. It is useful when a Rails controller action does little more than fetch several records from the database and present them in some other data format (like JSON or CSV).
|
4
|
+
|
5
|
+
Let's take an example. Suppose you have an action like this:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
def index
|
9
|
+
@messages = Message.created_by(current_user).after(3.weeks.ago)
|
10
|
+
render json: @messages.map { |message|
|
11
|
+
{ id: message.id,
|
12
|
+
postedAt: message.created_at,
|
13
|
+
text: message.text } }
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
This, of course, _instantiates_ a `Message` for every result, though we aren't really using a lot of ActiveRecord's features (in this action, at least). We instantiate all those objects on line 3 and then throw them away immediately afterward.
|
18
|
+
|
19
|
+
We can skip that step by using `pluck`:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
def index
|
23
|
+
@messages = Message.created_by(current_user).after(3.weeks.ago)
|
24
|
+
render json: @messages.pluck(:id, :created_at, :text)
|
25
|
+
.map { |id, created, text|
|
26
|
+
{ id: id,
|
27
|
+
postedAt: created_at,
|
28
|
+
text: text } }
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
In many cases, this is significantly faster.
|
33
|
+
|
34
|
+
But, now, if we needed to present a new attribute (say, `channel`), we have to write it _four_ times:
|
35
|
+
|
36
|
+
```diff
|
37
|
+
def index
|
38
|
+
@messages = Message.created_by(current_user).after(3.weeks.ago)
|
39
|
+
- render json: @messages.pluck(:id, :created_at, :text)
|
40
|
+
+ render json: @messages.pluck(:id, :created_at, :text, :channel)
|
41
|
+
- .map { |id, created, text|
|
42
|
+
+ .map { |id, created, text, channel|
|
43
|
+
{ id: id,
|
44
|
+
postedAt: created_at,
|
45
|
+
- text: text } }
|
46
|
+
+ text: text,
|
47
|
+
+ channel: channel } }
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
When we're presenting large or complex objects, the list of attributes we send to `pluck` or arguments we declare in the block passed to `map` can get pretty awkward.
|
52
|
+
|
53
|
+
The `PluckMap::Presenter` DSL is just a shortcut for generating the above pluck-map pattern in a more succinct way. The example above looks like this:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
def index
|
57
|
+
@messages = Message.created_by(current_user).after(3.weeks.ago)
|
58
|
+
presenter = PluckMap::Presenter.new do |q|
|
59
|
+
q.id
|
60
|
+
q.postedAt select: :created_at
|
61
|
+
q.text
|
62
|
+
q.channel
|
63
|
+
end
|
64
|
+
render json: presenter.to_h
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
This DSL also makes it easy to make fields optional:
|
69
|
+
|
70
|
+
```diff
|
71
|
+
def index
|
72
|
+
@messages = Message.created_by(current_user).after(3.weeks.ago)
|
73
|
+
presenter = PluckMap::Presenter.new do |q|
|
74
|
+
q.id
|
75
|
+
q.postedAt select: :created_at
|
76
|
+
q.text
|
77
|
+
- q.channel
|
78
|
+
+ q.channel if params[:fields] =~ /channel/
|
79
|
+
end
|
80
|
+
render json: presenter.to_h
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
|
85
|
+
## Installation
|
86
|
+
|
87
|
+
Add this line to your application's Gemfile:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
gem "pluck_map"
|
91
|
+
```
|
92
|
+
|
93
|
+
And then execute:
|
94
|
+
|
95
|
+
$ bundle
|
96
|
+
|
97
|
+
Or install it yourself as:
|
98
|
+
|
99
|
+
$ gem install pluck_map
|
100
|
+
|
101
|
+
|
102
|
+
## Development
|
103
|
+
|
104
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake false` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
105
|
+
|
106
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
107
|
+
|
108
|
+
## Contributing
|
109
|
+
|
110
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/boblail/pluck_map.
|
111
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pluck_map"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module PluckMap
|
2
|
+
class Attribute
|
3
|
+
attr_reader :id, :selects, :name, :alias, :block
|
4
|
+
|
5
|
+
def initialize(id, options={})
|
6
|
+
@id = id
|
7
|
+
@selects = Array(options.fetch(:select, id))
|
8
|
+
@name = options.fetch(:as, id)
|
9
|
+
@alias = name.to_s.gsub('_', ' ')
|
10
|
+
@block = options[:map]
|
11
|
+
raise ArgumentError, "You must select at least one column" if selects.empty?
|
12
|
+
raise ArgumentError, "You must define a block if you are going to select " <<
|
13
|
+
"more than one expression from the database" if selects.length > 1 && !block
|
14
|
+
end
|
15
|
+
|
16
|
+
def apply(object)
|
17
|
+
block.call(*object)
|
18
|
+
end
|
19
|
+
|
20
|
+
# These are the names of the values that are returned
|
21
|
+
# from the database (every row returned by the database
|
22
|
+
# will be a hash of key-value pairs)
|
23
|
+
#
|
24
|
+
# If we are only selecting one thing from the database
|
25
|
+
# then the PluckMapPresenter will automatically alias
|
26
|
+
# the select-expression, so the key will be the alias.
|
27
|
+
def keys
|
28
|
+
selects.length == 1 ? [self.alias] : selects
|
29
|
+
end
|
30
|
+
|
31
|
+
def no_map?
|
32
|
+
block.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
# When the PluckMapPresenter performs the query, it will
|
36
|
+
# receive an array of rows. Each row will itself be an
|
37
|
+
# array of values.
|
38
|
+
#
|
39
|
+
# This method constructs a Ruby expression that will
|
40
|
+
# extract the appropriate values from each row that
|
41
|
+
# correspond to this Attribute.
|
42
|
+
#
|
43
|
+
# The array of values will be correspond to the array
|
44
|
+
# of keys. This method determines which values pertain
|
45
|
+
# to it by figuring out which order its keys were selected in
|
46
|
+
def to_ruby(keys)
|
47
|
+
indexes = self.keys.map { |key| keys.index(key) }
|
48
|
+
return "values[#{indexes[0]}]" if indexes.length == 1 && !block
|
49
|
+
ruby = "values.values_at(#{indexes.join(", ")})"
|
50
|
+
ruby = "invoke(:\"#{id}\", #{ruby})" if block
|
51
|
+
ruby
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "pluck_map/version"
|
2
|
+
require "pluck_map/attribute"
|
3
|
+
|
4
|
+
module PluckMap
|
5
|
+
class Presenter
|
6
|
+
attr_reader :attributes
|
7
|
+
|
8
|
+
def initialize(&block)
|
9
|
+
@attributes = []
|
10
|
+
if block.arity == 1
|
11
|
+
block.call(self)
|
12
|
+
else
|
13
|
+
instance_eval(&block)
|
14
|
+
end
|
15
|
+
@initialized = true
|
16
|
+
|
17
|
+
@attributes_by_id = attributes.index_by(&:id).with_indifferent_access
|
18
|
+
@keys = attributes.flat_map(&:keys).uniq
|
19
|
+
|
20
|
+
define_presenters!
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(attribute_name, *args, &block)
|
24
|
+
return super if initialized?
|
25
|
+
attributes.push PluckMap::Attribute.new(attribute_name, args.extract_options!)
|
26
|
+
:attribute_added
|
27
|
+
end
|
28
|
+
|
29
|
+
def no_map?
|
30
|
+
attributes.all?(&:no_map?)
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def define_presenters!
|
36
|
+
define_to_h!
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialized?
|
40
|
+
@initialized
|
41
|
+
end
|
42
|
+
|
43
|
+
def pluck(query)
|
44
|
+
# puts "\e[95m#{query.select(*selects(query.table_name)).to_sql}\e[0m"
|
45
|
+
results = benchmark("pluck(#{query.table_name})") { query.pluck(*selects(query.table_name)) }
|
46
|
+
return results unless block_given?
|
47
|
+
benchmark("map(#{query.table_name})") { yield results }
|
48
|
+
end
|
49
|
+
|
50
|
+
def benchmark(title)
|
51
|
+
result = nil
|
52
|
+
ms = Benchmark.ms { result = yield }
|
53
|
+
Rails.logger.info "\e[33m#{title}: \e[1m%.1fms\e[0m" % ms
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
attr_reader :attributes_by_id, :keys
|
59
|
+
|
60
|
+
def selects(table_name)
|
61
|
+
attributes.flat_map do |attribute|
|
62
|
+
if attribute.selects.length != 1
|
63
|
+
attribute.selects
|
64
|
+
else
|
65
|
+
select = attribute.selects[0]
|
66
|
+
select = "\"#{table_name}\".\"#{select}\"" if select.is_a?(Symbol)
|
67
|
+
"#{select} AS \"#{attribute.alias}\""
|
68
|
+
end
|
69
|
+
end.uniq
|
70
|
+
end
|
71
|
+
|
72
|
+
def invoke(attribute_id, object)
|
73
|
+
attributes_by_id.fetch(attribute_id).apply(object)
|
74
|
+
end
|
75
|
+
|
76
|
+
def define_to_h!
|
77
|
+
ruby = <<-RUBY
|
78
|
+
def to_h(query)
|
79
|
+
pluck(query) do |results|
|
80
|
+
results.map { |values| values = Array(values); { #{attributes.map { |attribute| "#{attribute.name.inspect} => #{(attribute.to_ruby(keys))}"}.join(", ")} } }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
RUBY
|
84
|
+
# puts "\e[34m#{ruby}\e[0m" # <-- helps debugging PluckMapPresenter
|
85
|
+
class_eval ruby, __FILE__, __LINE__ - 7
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
data/lib/pluck_map.rb
ADDED
data/pluck_map.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "pluck_map/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pluck_map"
|
8
|
+
spec.version = PluckMapPresenter::VERSION
|
9
|
+
spec.authors = ["Bob Lail"]
|
10
|
+
spec.email = ["bob.lail@cph.org"]
|
11
|
+
|
12
|
+
spec.summary = "A DSL for presenting ActiveRecord::Relations without instantiating ActiveRecord models"
|
13
|
+
spec.homepage = "https://github.com/boblail/pluck_map"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
21
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pluck_map
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bob Lail
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- bob.lail@cph.org
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- ".travis.yml"
|
50
|
+
- Gemfile
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- bin/console
|
54
|
+
- bin/setup
|
55
|
+
- lib/pluck_map.rb
|
56
|
+
- lib/pluck_map/attribute.rb
|
57
|
+
- lib/pluck_map/presenter.rb
|
58
|
+
- lib/pluck_map/version.rb
|
59
|
+
- pluck_map.gemspec
|
60
|
+
homepage: https://github.com/boblail/pluck_map
|
61
|
+
licenses: []
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.4.8
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: A DSL for presenting ActiveRecord::Relations without instantiating ActiveRecord
|
83
|
+
models
|
84
|
+
test_files: []
|