capsula 0.0.3 → 0.0.4

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: c534b9fef1cf66293f27706a7399215013ca7d41f58b3fd7bbf6e1f4990bc0a8
4
- data.tar.gz: 16af229a6c5155c34e8fc4e91eac9911fae50c6cfd29e5b102aa14ebae28a07d
3
+ metadata.gz: c079ef240a2cfdd558fdf3edffa7987476f278ade0831c55947011fac809ba9b
4
+ data.tar.gz: 8d99d06ef13427f10e8bab16a2ba813a7621c65e1348deae30dd3e2d8e253591
5
5
  SHA512:
6
- metadata.gz: 3480eb2df25870f7e9fd507847897cdc8cee3037bbef3c25312ef822218bf9ac54e34ecb4e7acfbf5609bc18334300fb8b71f799b5ca107989b14e17368aef12
7
- data.tar.gz: 2240b2987901d94a39d5407f0b4caa9ad943987d0e0bfe1ae4433cb1dbed18673977a13f92eaa65a4ae14c4dd52c53a70e68b43c0501e3a98ad3f636986d5643
6
+ metadata.gz: 2e9519cd42c384e75c56ec4e342d3506da9ee0ffa83a231a36eb82d87b324bcf637bc0112c594778fe1a1c22723acefe4eb2122d824a69ca639e25a3300bd9e9
7
+ data.tar.gz: e5edde7d165a6eb1f075222d6be927c98c4c39b86af12765eb8abebc4ea03eb8fdf9db8fb5308b18d22aad49a09ed143e96f34ff37346db4027cc3aa8aeef466
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capsula (0.0.2)
4
+ capsula (0.0.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,42 +1,97 @@
1
1
  # Capsula
2
- The tool for encapsulating (preloading, including) related objects into **any** object.
2
+ The tool for preloading related objects into **any** object to prevent N+1 queries.
3
3
 
4
- ## How to use:
5
-
6
- Add to Gemfile:
4
+ ## INSTALL
7
5
 
8
6
  ```
9
7
  gem 'capsula'
10
8
  ```
11
9
 
12
- Then:
10
+ ## USE
13
11
 
14
12
  ```ruby
15
- starships = <get_your_space_fleet>
13
+ require 'capsula'
14
+
15
+ # our objects
16
+ $cars = [ {name: 'A', brand_id: 3}, {name: 'B', brand_id: 1}, {name: 'C', brand_id: 1} ]
17
+ $brands = [ {id: 1, name: 'Ferrari'}, {id: 2, name: 'Lamborghini'}, {id: 3, name: 'Rolls-Royce'} ]
18
+
19
+ # let's preload cars with brands
20
+
21
+ # our capsula-preloader
22
+ class CarPreloader < Capsula::Base
23
+ plan_for :brand,
24
+ src_key: -> (car) { car[:brand_id] },
25
+ dst_key: -> (brand) { brand[:id] },
26
+ dst_loader: -> (ids, opt) { $brands.select { |b| ids.include?(b[:id]) } }
27
+ end
28
+
29
+ # ok, preload!
30
+ $cars = CarPreloader.new($cars).plans(:brand).encapsulate
16
31
 
17
- starships = StarshipsEncapsulator.new(starships).plans(
18
- :fuel, :oxygen, :food, { spaceman: [:space_suit] }
32
+ $cars
33
+ # => [{:name=>"A", :brand_id=>3}, {:name=>"B", :brand_id=>1}, {:name=>"C", :brand_id=>1}]
34
+
35
+ # hm... where is brands?
36
+
37
+ $cars.first.brand
38
+ # => {:id=>3, :name=>"Rolls-Royce"}
39
+
40
+ $cars[0].brand
41
+ # => {:id=>3, :name=>"Rolls-Royce"}
42
+ $cars[1].brand
43
+ # => {:id=>1, :name=>"Ferrari"}
44
+ $cars[2].brand
45
+ # => {:id=>1, :name=>"Ferrari"}
46
+
47
+ # so, source objects leave untouched, but was wrapped for methods interception
48
+ ```
49
+
50
+ ### ActiveRecord case:
51
+
52
+ ```ruby
53
+ cars = Cars.where(id: [1,2,3]).to_a
54
+
55
+ cars = CarPreloader.new(cars).plans(
56
+ :fuel, :food, { driver: [:car_keys, :sunglasses] }
19
57
  ).encapsulate
20
58
 
21
- # Now let's get a suit for our spaceman:
22
- starships.first&.spaceman&.space_suit
23
- => <awesome space suit>
24
- # no actual action was triggered,
59
+ cars.first.driver.car_keys
60
+ => <car_keys>
61
+ # no actual action (any SQL query) was triggered,
25
62
  # immediate result was received from Capsula's wrapper
26
63
  ```
27
64
 
28
- Let's see how `StarshipsEncapsulator` looks:
65
+ Let's see how `CarPreloader` looks:
29
66
 
30
67
  ```ruby
31
68
  class StarshipsEncapsulator < Capsula::Base
32
- plan_for :fuel,
33
- src_key: :fuel_id,
34
- dst_key: :id,
35
- dst_loader: ->(ids,opt){ MyFuelStation.find_fuel_by_ids(ids) }
36
- # Example loader for ActiveRecord model Fuel:
37
- # dst_loader: ->(ids,opt){ Fuel.where(id: ids).includes(opt[:plans]).to_a }
38
-
39
- # Plans for other relations...
69
+ plan_for :driver,
70
+ src_key: :driver_id, # it's default value, so can be skipped
71
+ dst_key: :id, # it's default value, so can be skipped
72
+ # Example loader for ActiveRecord model Driver:
73
+ dst_loader: -> (ids, opt) {
74
+ Driver.where(id: ids).includes(opt[:plans]).to_a
75
+ }
76
+
77
+ # # Plans for other relations:
78
+ # plan_for :fuel, ...
79
+ # plan_for :food, ...
80
+ end
81
+ ```
82
+
83
+ ### has_many example
84
+
85
+ You can use custom encapsulator, but stantard encapsulator understand you if dst_key be placed inside array:
86
+
87
+ ```ruby
88
+ class Sea < Capsula::Base
89
+ plan_for :crabs,
90
+ src_key: :name,
91
+ dst_key: [:sea_name], # key inside array signals about has_many relation
92
+ dst_loader: -> (sea_names, opt) {
93
+ Crab.where(sea_name: sea_names).includes(opt[:plans]).to_a
94
+ }
40
95
  end
41
96
  ```
42
97
 
@@ -44,7 +99,7 @@ end
44
99
 
45
100
  **default values:**
46
101
 
47
- Definition for `src_key` and `dst_key` can be skipped if they values are `:fuel_id` (`<key_name>_id`) and `:id`
102
+ Definition for `src_key` and `dst_key` can be skipped if they values are `:driver_id` (`<key_name>_id`) and `:id`
48
103
 
49
104
  **lambdas:**
50
105
 
@@ -94,12 +149,13 @@ class CustomLoader
94
149
  # This method is triggered by Capsula
95
150
  def collect_ids_and_load_relations
96
151
  ids = @items.map{ |i| i.fuel_id }
97
- @store = Fuel.where(id: ids).to_a
152
+ preloads = Fuel.where(id: ids).to_a
153
+ @store = preloads.index_by(&:id)
98
154
  end
99
155
 
100
156
  # This method is calling by Capsula during encapsulation
101
157
  def get_preloads_for_object starship
102
- @store.find { |fuel| starship.fuel_id == fuel.id }
158
+ @store[starship.fuel_id]
103
159
  end
104
160
  end
105
161
 
data/capsula.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'capsula'
3
- s.version = '0.0.3'
4
- s.date = '2019-02-04'
3
+ s.version = '0.0.4'
4
+ s.date = '2019-05-29'
5
5
  s.summary = 'Encapsulating tool'
6
6
  s.description = 'The tool for encapsulating (preloading, including) related objects'
7
7
  s.authors = ['Rodion V']
@@ -108,8 +108,14 @@ module Capsula
108
108
  col =
109
109
  if ids.any?
110
110
  preloads = dec[:dst_loader].call(ids,opt)
111
- # build hash: {id1 => Obj1, ..., idN => ObjN }
112
- index_array_by(preloads) { |el| get_value(el, dec[:dst_key]) }
111
+ dst_key = dec[:dst_key]
112
+ if dst_key.is_a?(Array)
113
+ # build hash: {id1 => [Obj1, ...], idN => [ObjN] }
114
+ preloads.group_by { |o| get_value(o, dst_key.first) }
115
+ else
116
+ # build hash: {id1 => Obj1, ..., idN => ObjN }
117
+ index_array_by(preloads) { |el| get_value(el, dst_key) }
118
+ end
113
119
  else
114
120
  {}
115
121
  end
@@ -130,12 +136,16 @@ module Capsula
130
136
  def native_encapsulation plan_key, preloaded_collection, declaration
131
137
  col = preloaded_collection
132
138
  dec = declaration
139
+ is_dst_key_array = dec[:dst_key].is_a?(Array)
133
140
 
134
141
  @items.each do |i|
135
142
  src_id = get_value(i, dec[:src_key])
136
143
 
137
144
  val =
138
- if src_id.is_a?(Array)
145
+ if is_dst_key_array
146
+ # src has many dst
147
+ col[src_id] || []
148
+ elsif src_id.is_a?(Array)
139
149
  # if object has many links to related objects (Array)
140
150
  col.values_at(*src_id)
141
151
  else
@@ -151,6 +161,8 @@ module Capsula
151
161
  case key_or_lambda
152
162
  when ::Symbol, ::String
153
163
  _object_.send(key_or_lambda)
164
+ when ::Array
165
+ _object_.send(key_or_lambda[0])
154
166
  when ::Proc
155
167
  key_or_lambda.call(_object_)
156
168
  else
data/spec/capsula_spec.rb CHANGED
@@ -2,10 +2,11 @@ require 'capsula'
2
2
 
3
3
  RSpec.describe Capsula do
4
4
  let(:klass){ Class.new(Capsula::Base) }
5
- let(:a) { Struct.new(:b_id) }
6
- let(:b) { Struct.new(:id) }
7
5
 
8
6
  describe 'User can' do
7
+ let(:a) { Struct.new(:b_id) }
8
+ let(:b) { Struct.new(:id) }
9
+
9
10
  it 'declare new encapsulation plan' do
10
11
  klass.plan_for(:b, src_key: :b_id, dst_key: :id, dst_loader: ->(i,o){})
11
12
  plans = klass.instance_variable_get('@plans_declarations')
@@ -82,7 +83,32 @@ RSpec.describe Capsula do
82
83
  expect(objects.first).to be_a(Capsula::Wrapper)
83
84
  expect(objects.first.b.id).to be == objects.first.b_id
84
85
  end
86
+ end
87
+
88
+ describe 'has_many plan' do
89
+ let(:a) { Struct.new(:id) }
90
+ let(:b) { Struct.new(:a_id) }
91
+
92
+ it 'working' do
93
+ b_store = [1,2,2,3].map { |a_id| b.new(a_id) }
94
+ klass.plan_for(
95
+ :bs,
96
+ src_key: :id,
97
+ dst_key: [:a_id],
98
+ dst_loader: ->(ids,opt){ b_store.select { |b| ids.include?(b.a_id) } }
99
+ )
85
100
 
101
+ as = [2,3,4].map { |id| a.new(id) }
102
+ as = klass.new(as).plans(:bs).encapsulate
103
+
104
+ expect(as[0]).to respond_to(:bs)
105
+ expect(as[0]).to be_a(Capsula::Wrapper)
106
+ expect(as[0].bs.count).to eq(2)
107
+ expect(as[1].bs.count).to eq(1)
108
+ expect(as[2].bs.count).to eq(0)
109
+ expect(as[0].bs[0].a_id).to eq(as[0].id)
110
+ expect(as[1].bs[0].a_id).to eq(as[1].id)
111
+ end
86
112
  end
87
113
 
88
114
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capsula
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodion V
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-04 00:00:00.000000000 Z
11
+ date: 2019-05-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: The tool for encapsulating (preloading, including) related objects
14
14
  email: rodion.v@devaer.com