pluck_map 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: baee1fa797afc235f781a0b9d2492a8796ff54b3e90fd795bda2fd772c70a146
4
- data.tar.gz: a90a51ca0fb57d382f00228f31ace8e2157153aa4bf8f29f75589a6b6b6812f9
3
+ metadata.gz: c84ee8f4db192127a69450c81189315686ee14b23e65bfd08d49ee14c09e9d6c
4
+ data.tar.gz: d00b45929552234fe71457bf40649f8cdf6344198d278a4168760bf694c86e65
5
5
  SHA512:
6
- metadata.gz: 8ca7ae6a27609c8e26edfe2f33d9929773b2fc1bba4ee193633def8f4f5637d6914654c36cfc67812fb1debae1d0095fa04636c4734e9b31f98f6582e16cbff2
7
- data.tar.gz: cb720743428655889f2ea30e7e4de92f27a07564faf8190b18d6b44dc7165e971cbd7e027c252aedcb39ae85e1de8c16346f147142424c521ddeb71c2096c8dc
6
+ metadata.gz: 35eb0d21063e78cf8ea5968e93faa715fffba2198f4b54cfa5f0ade709bd375741c1b29dc27d03cdc8a5bbce99b5afa53fdc6e34f1fdabcfecb7f3a7cb117410
7
+ data.tar.gz: 909ab9afe1a0818bddaecb2406d8e69a5c4d6e64ee0949509ef3790ddaf167bf6e83d04ddff7e9ea540e10cebe710641983659cefb8ba48838dc55e82b10bbe2
@@ -1,3 +1,8 @@
1
+ ## v0.4.0 (2019 Apr 16)
2
+
3
+ * REFACTOR: Introduced several DEPRECATION WARNINGs that will be harvested in v1.0.0 (@boblail)
4
+ * REFACTOR: Changed the implementation of the private method `selects` (@boblail)
5
+
1
6
  ## v0.3.0 (2019 Apr 7)
2
7
 
3
8
  * IMPROVEMENT: Add support for MySQL (@boblail)
data/README.md CHANGED
@@ -3,28 +3,30 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/pluck_map.svg)](https://rubygems.org/gems/pluck_map)
4
4
  [![Build Status](https://travis-ci.org/boblail/pluck_map.svg)](https://travis-ci.org/boblail/pluck_map)
5
5
 
6
- 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).
6
+ This library provides a DSL for presenting ActiveRecord::Relations without instantiating ActiveRecord models. 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).
7
7
 
8
- Let's take an example. Suppose you have an action like this:
8
+ Suppose you have an action like this:
9
9
 
10
10
  ```ruby
11
11
  def index
12
- @messages = Message.created_by(current_user).after(3.weeks.ago)
13
- render json: @messages.map { |message|
12
+ messages = Message.created_by(current_user).after(3.weeks.ago)
13
+
14
+ render json: messages.map { |message|
14
15
  { id: message.id,
15
16
  postedAt: message.created_at,
16
17
  text: message.text } }
17
18
  end
18
19
  ```
19
20
 
20
- 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.
21
+ This instantiates a `Message` for every result, gets the attributes out of it, and then immediately discards it.
21
22
 
22
- We can skip that step by using `pluck`:
23
+ We can skip that unnecessary instantiation by using `pluck`:
23
24
 
24
25
  ```ruby
25
26
  def index
26
- @messages = Message.created_by(current_user).after(3.weeks.ago)
27
- render json: @messages.pluck(:id, :created_at, :text)
27
+ messages = Message.created_by(current_user).after(3.weeks.ago)
28
+
29
+ render json: messages.pluck(:id, :created_at, :text)
28
30
  .map { |id, created, text|
29
31
  { id: id,
30
32
  postedAt: created_at,
@@ -32,15 +34,15 @@ We can skip that step by using `pluck`:
32
34
  end
33
35
  ```
34
36
 
35
- In many cases, this is significantly faster.
37
+ This tends to be about one order of magnitude faster the first example.
36
38
 
37
- But, now, if we needed to present a new attribute (say, `channel`), we have to write it _four_ times:
39
+ It is straightforward but verbose (we repeat the attribute names at least three times) and changing a block like this produces noisy diffs:
38
40
 
39
41
  ```diff
40
42
  def index
41
- @messages = Message.created_by(current_user).after(3.weeks.ago)
42
- - render json: @messages.pluck(:id, :created_at, :text)
43
- + render json: @messages.pluck(:id, :created_at, :text, :channel)
43
+ messages = Message.created_by(current_user).after(3.weeks.ago)
44
+ - render json: messages.pluck(:id, :created_at, :text)
45
+ + render json: messages.pluck(:id, :created_at, :text, :channel)
44
46
  - .map { |id, created, text|
45
47
  + .map { |id, created, text, channel|
46
48
  { id: id,
@@ -51,28 +53,30 @@ But, now, if we needed to present a new attribute (say, `channel`), we have to w
51
53
  end
52
54
  ```
53
55
 
54
- 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.
56
+ And when we're presenting large or complex objects, the list of attributes we send to `pluck` or arguments we declare in the `map` block can get unwieldy!
55
57
 
56
- 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:
58
+ The `PluckMap::Presenter` is simply a shorthand for generating the above pluck-map pattern. Using it, we could write our example like this:
57
59
 
58
60
  ```ruby
59
61
  def index
60
- @messages = Message.created_by(current_user).after(3.weeks.ago)
62
+ messages = Message.created_by(current_user).after(3.weeks.ago)
61
63
  presenter = PluckMap::Presenter.new do |q|
62
64
  q.id
63
65
  q.postedAt select: :created_at
64
66
  q.text
65
67
  q.channel
66
68
  end
67
- render json: presenter.to_h
69
+ render json: presenter.to_h(messages)
68
70
  end
69
71
  ```
70
72
 
73
+ Using that definition, `PluckMap::Presenter` dynamically generates a `.to_h` method that is implemented exactly like the example above that uses `.pluck` and `.map`.
74
+
71
75
  This DSL also makes it easy to make fields optional:
72
76
 
73
77
  ```diff
74
78
  def index
75
- @messages = Message.created_by(current_user).after(3.weeks.ago)
79
+ messages = Message.created_by(current_user).after(3.weeks.ago)
76
80
  presenter = PluckMap::Presenter.new do |q|
77
81
  q.id
78
82
  q.postedAt select: :created_at
@@ -80,7 +84,7 @@ This DSL also makes it easy to make fields optional:
80
84
  - q.channel
81
85
  + q.channel if params[:fields] =~ /channel/
82
86
  end
83
- render json: presenter.to_h
87
+ render json: presenter.to_h(messages)
84
88
  end
85
89
  ```
86
90
 
@@ -104,10 +108,11 @@ Or install it yourself as:
104
108
 
105
109
  ## Development
106
110
 
107
- 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.
111
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
108
112
 
109
113
  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).
110
114
 
115
+
111
116
  ## Contributing
112
117
 
113
118
  Bug reports and pull requests are welcome on GitHub at https://github.com/boblail/pluck_map.
data/Rakefile CHANGED
@@ -43,6 +43,7 @@ def run_without_aborting(*tasks)
43
43
  errors = []
44
44
 
45
45
  tasks.each do |task|
46
+ puts task
46
47
  Rake::Task[task].invoke
47
48
  rescue Exception
48
49
  errors << task
@@ -1,5 +1,11 @@
1
1
  require "pluck_map/version"
2
2
  require "pluck_map/presenter"
3
+ require "pluck_map/null_logger"
3
4
 
4
5
  module PluckMap
6
+ class << self
7
+ attr_accessor :logger
8
+ end
9
+
10
+ @logger = defined?(Rails) ? Rails.logger : PluckMap::NullLogger.new
5
11
  end
@@ -1,12 +1,12 @@
1
1
  module PluckMap
2
2
  class Attribute
3
- attr_reader :id, :selects, :name, :alias, :block
3
+ attr_reader :id, :selects, :name, :value, :block
4
+ attr_accessor :indexes
4
5
 
5
6
  def initialize(id, options={})
6
7
  @id = id
7
8
  @selects = Array(options.fetch(:select, id))
8
9
  @name = options.fetch(:as, id)
9
- @alias = name.to_s.tr("_", " ")
10
10
  @block = options[:map]
11
11
 
12
12
  if options.key? :value
@@ -23,17 +23,6 @@ module PluckMap
23
23
  block.call(*object)
24
24
  end
25
25
 
26
- # These are the names of the values that are returned
27
- # from the database (every row returned by the database
28
- # will be a hash of key-value pairs)
29
- #
30
- # If we are only selecting one thing from the database
31
- # then the PluckMapPresenter will automatically alias
32
- # the select-expression, so the key will be the alias.
33
- def keys
34
- selects.length == 1 ? [self.alias] : selects
35
- end
36
-
37
26
  def no_map?
38
27
  block.nil?
39
28
  end
@@ -45,17 +34,37 @@ module PluckMap
45
34
  # This method constructs a Ruby expression that will
46
35
  # extract the appropriate values from each row that
47
36
  # correspond to this Attribute.
48
- #
49
- # The array of values will be correspond to the array
50
- # of keys. This method determines which values pertain
51
- # to it by figuring out which order its keys were selected in
52
- def to_ruby(keys)
37
+ def to_ruby(selects = nil)
38
+ if selects
39
+ puts "DEPRECATION WARNING: PluckMap::Attribute#to_ruby no longer requires an argument. Replace `attribute.to_ruby(keys)` with `attribute.to_ruby`."
40
+ end
41
+
53
42
  return @value.inspect if defined?(@value)
54
- indexes = self.keys.map { |key| keys.index(key) }
55
43
  return "values[#{indexes[0]}]" if indexes.length == 1 && !block
56
44
  ruby = "values.values_at(#{indexes.join(", ")})"
57
45
  ruby = "invoke(:\"#{id}\", #{ruby})" if block
58
46
  ruby
59
47
  end
48
+
49
+
50
+
51
+ def values
52
+ [id, selects, name, value, block]
53
+ end
54
+
55
+ def ==(other)
56
+ return false if self.class != other.class
57
+ self.values == other.values
58
+ end
59
+
60
+ def hash
61
+ values.hash
62
+ end
63
+
64
+ def eql?(other)
65
+ return true if self.equal?(other)
66
+ return false if self.class != other.class
67
+ self.values.eql?(other.values)
68
+ end
60
69
  end
61
70
  end
@@ -0,0 +1,28 @@
1
+ require "pluck_map/attribute"
2
+
3
+ module PluckMap
4
+ class AttributeBuilder
5
+
6
+ def self.build(&block)
7
+ builder = self.new
8
+ if block.arity == 1
9
+ block.call(builder)
10
+ else
11
+ builder.instance_eval(&block)
12
+ end
13
+ builder.instance_variable_get(:@attributes).freeze
14
+ end
15
+
16
+ def initialize
17
+ @attributes = []
18
+ end
19
+
20
+ def method_missing(attribute_name, *args)
21
+ options = args.extract_options!
22
+ options[:value] = args.first unless args.empty?
23
+ @attributes.push PluckMap::Attribute.new(attribute_name, options)
24
+ :attribute_added
25
+ end
26
+
27
+ end
28
+ end
@@ -1,37 +1,33 @@
1
- require "pluck_map/version"
2
- require "pluck_map/attribute"
3
- require "pluck_map/null_logger"
1
+ require "pluck_map/attribute_builder"
2
+ require "pluck_map/presenters"
4
3
 
5
4
  module PluckMap
6
5
  class Presenter
7
- attr_reader :attributes
6
+ include HashPresenter
8
7
 
9
- @logger = defined?(Rails) ? Rails.logger : PluckMap::NullLogger.new
10
- class << self
11
- attr_accessor :logger
12
- end
8
+ attr_reader :attributes
13
9
 
14
10
  def initialize(&block)
15
- @attributes = []
16
- if block.arity == 1
17
- block.call(self)
18
- else
19
- instance_eval(&block)
11
+ @attributes = PluckMap::AttributeBuilder.build(&block)
12
+
13
+ @attributes_by_id = {}
14
+ @selects = []
15
+ attributes.each do |attribute|
16
+ attribute.indexes = attribute.selects.map do |select|
17
+ selects.find_index(select) || begin
18
+ selects.push(select)
19
+ selects.length - 1
20
+ end
21
+ end
22
+ attributes_by_id[attribute.id] = attribute
20
23
  end
21
- @initialized = true
22
-
23
- @attributes_by_id = attributes.index_by(&:id).with_indifferent_access
24
- @keys = attributes.flat_map(&:keys).uniq
25
24
 
26
- define_presenters!
27
- end
28
-
29
- def method_missing(attribute_name, *args, &block)
30
- return super if initialized?
31
- options = args.extract_options!
32
- options[:value] = args.first unless args.empty?
33
- attributes.push PluckMap::Attribute.new(attribute_name, options)
34
- :attribute_added
25
+ if respond_to?(:define_presenters!, true)
26
+ puts "DEPRECATION WARNING: `define_presenters!` is deprecated; instead mix in a module that implements your presenter method (e.g. `to_h`). Optionally have the method redefine itself the first time it is called."
27
+ # because overridden `define_presenters!` will probably call `super`
28
+ PluckMap::Presenter.class_eval 'protected def define_presenters!; end'
29
+ define_presenters!
30
+ end
35
31
  end
36
32
 
37
33
  def no_map?
@@ -40,17 +36,9 @@ module PluckMap
40
36
 
41
37
  protected
42
38
 
43
- def define_presenters!
44
- define_to_h!
45
- end
46
-
47
- def initialized?
48
- @initialized
49
- end
50
-
51
39
  def pluck(query)
52
- # puts "\e[95m#{query.select(*selects(query.table_name)).to_sql}\e[0m"
53
- results = benchmark("pluck(#{query.table_name})") { query.pluck(*selects(query.model)) }
40
+ # puts "\e[95m#{query.select(*selects).to_sql}\e[0m"
41
+ results = benchmark("pluck(#{query.table_name})") { query.pluck(*selects) }
54
42
  return results unless block_given?
55
43
  benchmark("map(#{query.table_name})") { yield results }
56
44
  end
@@ -58,39 +46,20 @@ module PluckMap
58
46
  def benchmark(title)
59
47
  result = nil
60
48
  ms = Benchmark.ms { result = yield }
61
- self.class.logger.info "\e[33m#{title}: \e[1m%.1fms\e[0m" % ms
49
+ PluckMap.logger.info "\e[33m#{title}: \e[1m%.1fms\e[0m" % ms
62
50
  result
63
51
  end
64
52
 
65
53
  private
66
- attr_reader :attributes_by_id, :keys
67
-
68
- def selects(model)
69
- attributes.flat_map do |attribute|
70
- if attribute.selects.length != 1
71
- attribute.selects
72
- else
73
- select = attribute.selects[0]
74
- select = "#{model.quoted_table_name}.#{model.connection.quote_column_name(select)}" if select.is_a?(Symbol)
75
- Arel.sql("#{select} AS \"#{attribute.alias}\"")
76
- end
77
- end.uniq
78
- end
54
+ attr_reader :attributes_by_id, :selects
79
55
 
80
56
  def invoke(attribute_id, object)
81
57
  attributes_by_id.fetch(attribute_id).apply(object)
82
58
  end
83
59
 
84
- def define_to_h!
85
- ruby = <<-RUBY
86
- def to_h(query)
87
- pluck(query) do |results|
88
- results.map { |values| values = Array(values); { #{attributes.map { |attribute| "#{attribute.name.inspect} => #{attribute.to_ruby(keys)}"}.join(", ")} } }
89
- end
90
- end
91
- RUBY
92
- # puts "\e[34m#{ruby}\e[0m" # <-- helps debugging PluckMapPresenter
93
- class_eval ruby, __FILE__, __LINE__ - 7
60
+ def keys
61
+ puts "DEPRECATION WARNING: PluckMap::Presenter#keys is deprecated with no replacement"
62
+ selects
94
63
  end
95
64
 
96
65
  end
@@ -0,0 +1 @@
1
+ require "pluck_map/presenters/to_h"
@@ -0,0 +1,22 @@
1
+ module PluckMap
2
+ module HashPresenter
3
+
4
+ def to_h(query)
5
+ define_to_h!
6
+ to_h(query)
7
+ end
8
+
9
+ private def define_to_h!
10
+ ruby = <<-RUBY
11
+ def to_h(query)
12
+ pluck(query) do |results|
13
+ results.map { |values| values = Array(values); { #{attributes.map { |attribute| "#{attribute.name.inspect} => #{attribute.to_ruby}" }.join(", ")} } }
14
+ end
15
+ end
16
+ RUBY
17
+ # puts "\e[34m#{ruby}\e[0m" # <-- helps debugging PluckMapPresenter
18
+ class_eval ruby, __FILE__, __LINE__ - 7
19
+ end
20
+
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module PluckMapPresenter
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -29,5 +29,6 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency "pg"
30
30
  spec.add_development_dependency "database_cleaner"
31
31
  spec.add_development_dependency "shoulda-context"
32
+ spec.add_development_dependency "rr"
32
33
  spec.add_development_dependency "pry"
33
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pluck_map
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Lail
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-04-07 00:00:00.000000000 Z
11
+ date: 2019-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rr
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  - !ruby/object:Gem::Dependency
168
182
  name: pry
169
183
  requirement: !ruby/object:Gem::Requirement
@@ -202,8 +216,11 @@ files:
202
216
  - gemfiles/rails_edge.gemfile
203
217
  - lib/pluck_map.rb
204
218
  - lib/pluck_map/attribute.rb
219
+ - lib/pluck_map/attribute_builder.rb
205
220
  - lib/pluck_map/null_logger.rb
206
221
  - lib/pluck_map/presenter.rb
222
+ - lib/pluck_map/presenters.rb
223
+ - lib/pluck_map/presenters/to_h.rb
207
224
  - lib/pluck_map/version.rb
208
225
  - pluck_map.gemspec
209
226
  homepage: https://github.com/boblail/pluck_map