pluck_map 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in pluck_map_presenter.gemspec
4
+ gemspec
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,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,3 @@
1
+ module PluckMapPresenter
2
+ VERSION = "0.1.0"
3
+ end
data/lib/pluck_map.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "pluck_map/version"
2
+ require "pluck_map/presenter"
3
+
4
+ module PluckMap
5
+ end
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: []