pluck_map 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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