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 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: []