knuckles 0.4.0 → 0.5.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
  SHA1:
3
- metadata.gz: 69ff93727664c8cb7f6c22d52542521b400f7421
4
- data.tar.gz: 88f1ad40c4d32591dfcc6b9d3e8c093d1c14a80a
3
+ metadata.gz: 96d10e0c441bc44adc48654b19878d2a7283cd7b
4
+ data.tar.gz: 4a9df62148abaec7d7fb161b37bf1bab7b511327
5
5
  SHA512:
6
- metadata.gz: 5bff88dba2e59d125dfa1c61ee050f191f690e1f1a12f704729e1f8e29b76bfe7a444e4bc550cd4a09d3777156114a10b8dc319f6594e92e83ca013ea17255aa
7
- data.tar.gz: e51e57e0df18ccaef4304ee9f78a47ee0472063be046592b883cf2e6f732ba68cc9803f2f9636ff09ad9e9b48006603fa1c36fb25203405e6d3f2b8144e50844
6
+ metadata.gz: 828e4d98d6f655aee4ed20fb0792af61920d9304775b23693b0f3cd87b7a6c4d4d68ceddc20a3895408eca23f88df1220239b74f705d415cf0b3f8b4525862fe
7
+ data.tar.gz: 4cb0da0daf47140f5ff953214f739d79de15de96f59134dadb803c02887a70c2cc6bf35b0719f5caf1493614e2685794ac58d236917866010cd0b32d54e674a0
@@ -1,3 +1,9 @@
1
+ ## v0.5.0 - 2016-07-08
2
+
3
+ * Added: Accept a `proc` or any callable object as the `keygen`. This simplifies
4
+ overriding the cache key on a per-instance basis.
5
+ * Added: Lots of documentation! All code has inline documentation now.
6
+
1
7
  ## v0.4.0 - 2016-05-11
2
8
 
3
9
  * Added: `Knuckles::Active::Hydrator`, a hydrator specifically designed to work
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  [![Build Status](https://travis-ci.org/sorentwo/knuckles.svg?branch=master)](https://travis-ci.org/sorentwo/knuckles)
2
2
  [![Coverage Status](https://coveralls.io/repos/github/sorentwo/knuckles/badge.svg?branch=master)](https://coveralls.io/github/sorentwo/knuckles?branch=master)
3
3
  [![Code Climate](https://codeclimate.com/github/sorentwo/knuckles/badges/gpa.svg)](https://codeclimate.com/github/sorentwo/knuckles)
4
+ [![Inline Docs](http://inch-ci.org/github/sorentwo/knuckles.svg?branch=master)](http://inch-ci.org/github/sorentwo/knuckles)
4
5
 
5
6
  # Knuckles (Because Sonic was Taken)
6
7
 
@@ -69,6 +70,144 @@ end
69
70
  With the top level module configured it is simple to jump right into rendering,
70
71
  but we'll look at configuring the pipeline first.
71
72
 
73
+ ## Understanding and Using Pipelines
74
+
75
+ Knuckles renders and serializes data through a series of stages composed into a
76
+ pipeline. Stages can easily be added or removed to control how data is
77
+ transformed. Here is a breakdown of the default stages and what their role is
78
+ within the pipeline.
79
+
80
+ #### Fetcher
81
+
82
+ The fetcher is responsible for bulk retrieval of data from the cache. Fetching
83
+ is done using a single `read_multi` operation, which is multiplexed in caches
84
+ like Redis or MemCached.
85
+
86
+ ```ruby
87
+ pipeline = Knuckles::Pipeline.new
88
+
89
+ pipeline.call(posts)
90
+ ```
91
+
92
+ #### Hydrator
93
+
94
+ Models that couldn't be retrieved from the cache will then be hydrated, a
95
+ process where the stripped down model that was given for fetching is replaced
96
+ with a full model with preloaded associations. The behavior of the hydrator
97
+ stage is entirely controlled by passing a Proc as the `hydrate` option. If the
98
+ `hydrate` proc is omitted hydration will be skipped. Skipping hydration is
99
+ useful if you want a simplified pipeline where full models and their
100
+ associations are preloaded before starting serialization.
101
+
102
+ See `Knuckles::Active::Hydrator` for an alternative `ActiveRecord` specific
103
+ hydrator. If you are using Knuckles within a Rais app, this is probably the
104
+ hydration stage you want to use.
105
+
106
+ ```ruby
107
+ # Using the standard hydrator
108
+ pipeline.call(posts, hydrator: -> (model) { model.fetch })
109
+
110
+ # Using active hydrator with a relation that has a `prepared` scope
111
+ pipeline.call(posts, relation: posts.prepared)
112
+ ```
113
+
114
+ #### Renderer
115
+
116
+ After un-cached models have been hydrated they can be rendered. Rendering is
117
+ synonymous with converting a model to a hash, like calling `as_json` on an
118
+ `ActiveRecord` model. Knuckles provides a minimal (but fast) view module that
119
+ can be used with the rendering step. Alternatively, if you're migrating from
120
+ `ActiveModelSerializers` you can pass in an AMS class instead.
121
+
122
+ ```ruby
123
+ # Using Knuckles::View
124
+ pipeline.call(models, view: PostView)
125
+
126
+ # Using ActiveModelSerializer
127
+ pipeline.call(models, view: PostSerializer)
128
+ ```
129
+
130
+ #### Writer
131
+
132
+ After un-cached models have been serialized they are ready to be cached for
133
+ future retrieval. Each fully serialized model is written to the cache in a
134
+ single `write_multi` operation if available (using Readthis, for example). Only
135
+ previously un-cached data will be written to the cache, making the writer a
136
+ no-op when all of the data was cached initially.
137
+
138
+ #### Enhancer
139
+
140
+ The enhancer modifies rendered data using proc passed through options. The
141
+ enhancer stage is critical to customizing the final output. For example, if
142
+ staff should have confidential data that regular users can't see you can enhance
143
+ the final values. Another use of enhancers is personalizing an otherwise generic
144
+ response.
145
+
146
+ ```ruby
147
+ # Removing staff only content from the rendered data
148
+ pipeline.call(posts,
149
+ scope: current_user,
150
+ enhancer: lambda do |result, options|
151
+ scope = options[:scope]
152
+
153
+ unless scope.staff?
154
+ result.delete_if { |key, _| key == "confidential" }
155
+ end
156
+
157
+ result
158
+ end
159
+ )
160
+ ```
161
+
162
+ #### Combiner
163
+
164
+ The combiner stage merges all of the individually rendered results into a single
165
+ hash. The output of this stage is a single object, ready to be serialized.
166
+
167
+ #### Dumper
168
+
169
+ The dumping process combines de-duplication and actual serialization. For every
170
+ top level key that is an array all of the children will have uniqueness
171
+ enforced. For example, if you had rendered a collection of posts that shared the
172
+ same author, you will only have a single author object serialized. Be aware that
173
+ the uniqueness check relies on the presence of an `id` key rather than full
174
+ object comparisons.
175
+
176
+ Dumping is the final stage of the pipeline. At this point you have a single
177
+ serialized payload in the format of your choice (JSON by default), ready to send
178
+ back as a response.
179
+
180
+ ## Customizing Pipelines
181
+
182
+ Pipelines stages can be removed, swapped out or otherwise tuned. An array of
183
+ stages can be passed when building a new pipeline. Here is an example of
184
+ creating a customized pipeline without any caching, hydration, or enhancing:
185
+
186
+ ```ruby
187
+ Knuckles::Pipeline.new(stages: [
188
+ Knuckles::Stages::Renderer,
189
+ Knuckles::Stages::Combiner,
190
+ Knuckles::Stages::Dumper
191
+ ])
192
+ ```
193
+
194
+ Or, perhaps you want to use the active hydrator instead:
195
+
196
+ ```ruby
197
+ Knuckles::Pipeline.new(stages: [
198
+ Knuckles::Stages::Fetcher,
199
+ Knuckles::Active::Hydrator,
200
+ Knuckles::Stages::Renderer,
201
+ Knuckles::Stages::Writer,
202
+ Knuckles::Stages::Enhancer,
203
+ Knuckles::Stages::Combiner,
204
+ Knuckles::Stages::Dumper
205
+ ])
206
+ ```
207
+
208
+ Note that once the pipeline is initialized the stages are frozen to prevent
209
+ modification.
210
+
72
211
  ## Defining Views for Rendering
73
212
 
74
213
  While you can use Knuckles with other serializers, you can also use the provided
@@ -95,6 +234,35 @@ end
95
234
 
96
235
  See `Knuckles::View` for more usage details.
97
236
 
237
+ ## Rendering in Rails
238
+
239
+ One driving factor of Knuckles is that code should be explicit. As a result
240
+ there isn't a default Railtie that will integrate Knuckles into the
241
+ `ActiveController` rendering process for you. Luckily there isn't much to
242
+ setting up a new pipeline for rendering. Add this to your
243
+ `ApplicationController` or an API specific controller:
244
+
245
+ ```ruby
246
+ def knuckles_render(relation, options)
247
+ Knuckles::Pipeline.new.call(relation, options)
248
+ end
249
+ ```
250
+
251
+ Now you can easily render responses:
252
+
253
+ ```ruby
254
+ def index
255
+ posts = posts.published.paginate(pagination_params)
256
+
257
+ render json: knuckles_render(
258
+ posts.select(:id, :updated_at),
259
+ relation: posts.prepared,
260
+ view: PostView,
261
+ scope: current_user,
262
+ )
263
+ end
264
+ ```
265
+
98
266
  ## Contributing
99
267
 
100
268
  1. Fork it ( https://github.com/sorentwo/knuckles/fork )
@@ -33,10 +33,13 @@ module Knuckles
33
33
  autoload :Pipeline, "knuckles/pipeline"
34
34
  autoload :View, "knuckles/view"
35
35
 
36
+ # Top level wrapper for stages that are expected to interact with `Active*`
37
+ # libraries, such as `ActiveModel`.
36
38
  module Active
37
39
  autoload :Hydrator, "knuckles/active/hydrator"
38
40
  end
39
41
 
42
+ # Top level wrapper for standard pipleline stages.
40
43
  module Stages
41
44
  autoload :Combiner, "knuckles/stages/combiner"
42
45
  autoload :Dumper, "knuckles/stages/dumper"
@@ -104,7 +107,8 @@ module Knuckles
104
107
  yield self
105
108
  end
106
109
 
107
- # @private
110
+ # Reset all configuration values back to `nil`, restoring them to the
111
+ # defaults. This is useful for testing because configuration is global.
108
112
  def reset!
109
113
  @cache = nil
110
114
  @keygen = nil
@@ -24,8 +24,8 @@ module Knuckles
24
24
  #
25
25
  # @example Hydrating missing objects
26
26
  #
27
- # prepared = [Post.new(1), Post.new(2)]
28
27
  # relation = Post.all.preload(:author, :comments)
28
+ # prepared = relation.select(:id, :updated_at)
29
29
  #
30
30
  # Knuckles::Active::Hydrator.call(prepared, relation: relation) #=>
31
31
  # # [{object: #Post<1>, cached?: false, ...
@@ -30,7 +30,7 @@ module Knuckles
30
30
  # Knuckles::Stages::Renderer,
31
31
  # Knuckles::Stages::Combiner,
32
32
  # Knuckles::Stages::Dumper
33
- # ]
33
+ # ])
34
34
  def initialize(stages: default_stages)
35
35
  @stages = stages.freeze
36
36
  end
@@ -2,10 +2,47 @@
2
2
 
3
3
  module Knuckles
4
4
  module Stages
5
+ # The combiner stage merges all of the individually rendered results into a
6
+ # single hash. The output of this stage is a single object with string keys
7
+ # and array values, ready to be serialized.
5
8
  module Combiner
6
9
  extend self
7
10
 
8
- def call(prepared, _)
11
+ # Merge all of the rendered data into a single hash. Each
12
+ # resulting value will be an array, even if there was only one
13
+ # value in the original rendered results.
14
+ #
15
+ # @param [Enumerable] prepared The prepared collection to be combined
16
+ # @param [Hash] _options Options aren't used, but are accepted
17
+ # to maintain a consistent interface
18
+ #
19
+ # @example Combining rendered data
20
+ #
21
+ # prepared = [
22
+ # {
23
+ # result: {
24
+ # author: {id: 1, name: "Michael"},
25
+ # posts: [{id: 1, title: "hello"}],
26
+ # }
27
+ # }, {
28
+ # result: {
29
+ # author: {id: 1, name: "Michael"},
30
+ # posts: [{id: 2, title: "there"}],
31
+ # }
32
+ # }
33
+ # ]
34
+ #
35
+ # Knuckles::Stage::Combiner.call(prepared, {}) #=> {
36
+ # "author" => [
37
+ # {id: 1, name: "Michael"}
38
+ # ],
39
+ # "posts" => [
40
+ # {id: 1, title: "hello"},
41
+ # {id: 2, title: "there"}
42
+ # ]
43
+ # }
44
+ #
45
+ def call(prepared, _options)
9
46
  prepared.each_with_object(array_backed_hash) do |hash, memo|
10
47
  hash[:result].each do |root, values|
11
48
  case values
@@ -16,6 +53,8 @@ module Knuckles
16
53
  end
17
54
  end
18
55
 
56
+ private
57
+
19
58
  def array_backed_hash
20
59
  Hash.new { |hash, key| hash[key] = [] }
21
60
  end
@@ -2,9 +2,23 @@
2
2
 
3
3
  module Knuckles
4
4
  module Stages
5
+ # The dumping process combines de-duplication and actual serialization. For
6
+ # every top level key that is an array all of the children will have
7
+ # uniqueness enforced. For example, if you had rendered a collection of
8
+ # posts that shared the same author, you will only have a single author
9
+ # object serialized. Be aware that the uniqueness check relies on the
10
+ # presence of an `id` key rather than full object comparisons.
5
11
  module Dumper
6
12
  extend self
7
13
 
14
+ # De-duplicate values in all keys and merge them into a single hash.
15
+ # Afterwards the complete hash is serialized using the serializer
16
+ # configured at `Knuckles.serializer`.
17
+ #
18
+ # @param [Enumerable<Hash>] objects A collection of hashes to be dumped
19
+ # @param [Hash] _options Options aren't used, but are accepted
20
+ # to maintain a consistent interface
21
+ #
8
22
  def call(objects, _options)
9
23
  Knuckles.serializer.dump(keys_to_arrays(objects))
10
24
  end
@@ -2,9 +2,38 @@
2
2
 
3
3
  module Knuckles
4
4
  module Stages
5
+ # The enhancer modifies rendered data using proc passed through options.
6
+ # The enhancer stage is critical to customizing the final output. For
7
+ # example, if staff should have confidential data that regular users can't
8
+ # see you can enhance the final values. Another use of enhancers is
9
+ # personalizing an otherwise generic response.
5
10
  module Enhancer
6
11
  extend self
7
12
 
13
+ # Modify all results using an `enhancer` proc.
14
+ #
15
+ # @param [Enumerable] prepared The prepared collection to be enhanced
16
+ # @option [Proc] :enhancer A `proc`, `lambda`, or any object that responds
17
+ # to `call`. Every complete `result` in the prepared collection will be
18
+ # passed to the enhancer.
19
+ #
20
+ # @example Removing tags unless the scope is staff
21
+ #
22
+ # enhancer = lambda do |result, options|
23
+ # scope = options[:scope]
24
+ #
25
+ # unless scope.staff?
26
+ # result.delete_if { |key, _| key == "tags" }
27
+ # end
28
+ #
29
+ # result
30
+ # end
31
+ #
32
+ # prepared = [{result: {"posts" => [], "tags" => []}}]
33
+ #
34
+ # Knuckles::Stages::Enhancer.call(prepared, enhancer: enhancer) #=>
35
+ # # [{result: {"posts" => []}}]
36
+ #
8
37
  def call(prepared, options)
9
38
  enhancer = options[:enhancer]
10
39
 
@@ -2,16 +2,44 @@
2
2
 
3
3
  module Knuckles
4
4
  module Stages
5
+ # The fetcher is responsible for bulk retrieval of data from the cache.
6
+ # Fetching is done using a single `read_multi` operation, which is
7
+ # multiplexed in caches like Redis or MemCached.
8
+ #
9
+ # The underlying cache *must* support `read_multi` for the stage to work.
5
10
  module Fetcher
6
11
  extend self
7
12
 
13
+ # Fetch all previously cached objects from the configured store.
14
+ #
15
+ # @param [Enumerable] prepared The prepared collection to fetch
16
+ # @option [Module] :keygen (Knuckles.keygen) The cache key generator used
17
+ # to construct an entries cache_key. It can be any object that responds
18
+ # to `expand_key`
19
+ #
20
+ # @example Provide a custom keygen
21
+ #
22
+ # keygen = Module.new do
23
+ # def self.expand_key(object)
24
+ # object.name
25
+ # end
26
+ # end
27
+ #
28
+ # Knuckles::Stages::Fetcher.call(prepared, keygen: keygen)
29
+ #
30
+ # @example Use a lambda as a keygen
31
+ #
32
+ # Knuckles::Stages::Fetcher.call(
33
+ # prepared,
34
+ # keygen: -> (object) { object.name }
35
+ # )
36
+ #
8
37
  def call(prepared, options)
9
38
  results = get_cached(prepared, options)
10
39
 
11
40
  prepared.each do |hash|
12
- result = results[hash[:key]]
13
- hash[:cached?] = !result.nil?
14
- hash[:result] = result
41
+ hash[:result] = results[hash[:key]]
42
+ hash[:cached?] = !hash[:result].nil?
15
43
  end
16
44
  end
17
45
 
@@ -20,11 +48,19 @@ module Knuckles
20
48
  def get_cached(prepared, options)
21
49
  kgen = options.fetch(:keygen, Knuckles.keygen)
22
50
  keys = prepared.map do |hash|
23
- hash[:key] = kgen.expand_key(hash[:object])
51
+ hash[:key] = expand_key(kgen, hash[:object])
24
52
  end
25
53
 
26
54
  Knuckles.cache.read_multi(*keys)
27
55
  end
56
+
57
+ def expand_key(keygen, object)
58
+ if keygen.respond_to?(:call)
59
+ keygen.call(object)
60
+ else
61
+ keygen.expand_key(object)
62
+ end
63
+ end
28
64
  end
29
65
  end
30
66
  end
@@ -2,9 +2,33 @@
2
2
 
3
3
  module Knuckles
4
4
  module Stages
5
+ # The hydrator converts minimal objects in a prepared collection into fully
6
+ # "hydrated" versions of the same record. For example, the initial `model`
7
+ # may only have the `id` and `updated_at` timestamp selected, which is
8
+ # ideal for fetching from the cache. If the object wasn't in the cache then
9
+ # all of the fields are needed for a complete rendering, so the hydration
10
+ # call will use the passed relation to fetch the full model and any
11
+ # associations.
12
+ #
13
+ # This is a generic hydrator suitable for any type of collection. If you
14
+ # are working with `ActiveRecord` you'll want to use the
15
+ # `Knuckles::Active::Hydrator` module instead.
5
16
  module Hydrator
6
17
  extend self
7
18
 
19
+ # Convert all uncached objects into their full representation.
20
+ #
21
+ # @param [Enumerable] prepared The prepared collection for processing
22
+ # @option [Proc, #call] :hydrate A proc used to load missing data for
23
+ # uncached objects
24
+ #
25
+ # @example Hydrating missing objects
26
+ #
27
+ # Knuckles::Hydrator.call(
28
+ # prepared,
29
+ # hydrate: -> (objects) { objects.each(&:fetch!) }
30
+ # )
31
+ #
8
32
  def call(prepared, options)
9
33
  hydrate = options[:hydrate]
10
34
 
@@ -2,9 +2,34 @@
2
2
 
3
3
  module Knuckles
4
4
  module Stages
5
+ # After un-cached models have been hydrated they can be rendered. Rendering
6
+ # is synonymous with converting a model to a hash, like calling `as_json`
7
+ # on an `ActiveRecord` model. Knuckles provides a minimal (but fast) view
8
+ # module that can be used with the rendering step. Alternatively, if you're
9
+ # migrating from `ActiveModelSerializers` you can pass in an AMS class
10
+ # instead.
5
11
  module Renderer
6
12
  extend self
7
13
 
14
+ # Serialize all un-cached objects into hashes.
15
+ #
16
+ # @param [Enumerable] objects The prepared collection to be rendered
17
+ # @option [Module] :view A `Knuckles::View` compliant module,
18
+ # it will be passed the object and any options. Alternately,
19
+ # a class compatible with the `ActiveModelSerializers` API.
20
+ #
21
+ # @example Using a Knuckles::View
22
+ #
23
+ # module PostView
24
+ # extend Knuckles::View
25
+ #
26
+ # def self.data(post, _options)
27
+ # {id: post.id, name: post.name}
28
+ # end
29
+ # end
30
+ #
31
+ # pipeline.call(models, view: PostView)
32
+ #
8
33
  def call(objects, options)
9
34
  view = options.fetch(:view)
10
35
 
@@ -2,10 +2,23 @@
2
2
 
3
3
  module Knuckles
4
4
  module Stages
5
+ # After un-cached models have been serialized they are ready to be cached
6
+ # for future retrieval. Each fully serialized model is written to the cache
7
+ # in a single `write_multi` operation if available (using Readthis, for
8
+ # example). Only previously un-cached data will be written to the cache,
9
+ # making the writer a no-op when all of the data was cached initially.
5
10
  module Writer
6
11
  extend self
7
12
 
8
- def call(objects, _)
13
+ # Write all serialized, but previously un-cached, data to the cache.
14
+ #
15
+ # @param [Enumerable] objects A collection of hashes to be serialized,
16
+ # each hash must have they keys `:key`, `:result`, and `:cached?`.
17
+ # @param [Hash] _options Options aren't used, but are accepted
18
+ # to maintain a consistent interface
19
+ # @return The original enumerable is returned unchanged
20
+ #
21
+ def call(objects, _options)
9
22
  if cache.respond_to?(:write_multi)
10
23
  write_multi(objects)
11
24
  else
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Knuckles
4
- VERSION = "0.4.0"
4
+ # The current version of Knuckles
5
+ VERSION = "0.5.0"
5
6
  end
@@ -46,8 +46,8 @@ module Knuckles
46
46
  # Convenience for combining the results of data and relations
47
47
  # into a single object.
48
48
  #
49
- # @param [Object] _object The object for serializing.
50
- # @param [Hash] _options The options to be used during serialization, i.e.
49
+ # @param [Object] object The object for serializing.
50
+ # @param [Hash] options The options to be used during serialization, i.e.
51
51
  # `:scope`
52
52
  #
53
53
  # @return [Hash] A hash representing the serialized object and relations.
@@ -24,6 +24,15 @@ RSpec.describe Knuckles::Stages::Fetcher do
24
24
 
25
25
  expect(pluck(results, :key)).to eq(["alpha"])
26
26
  end
27
+
28
+ it "allows using a lambda as a keygen" do
29
+ results = Knuckles::Stages::Fetcher.call(
30
+ prepare([Tag.new(1, "alpha")]),
31
+ keygen: -> (object) { object.name }
32
+ )
33
+
34
+ expect(pluck(results, :key)).to eq(["alpha"])
35
+ end
27
36
  end
28
37
 
29
38
  def pluck(enum, key)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knuckles
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Parker Selbert
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-11 00:00:00.000000000 Z
11
+ date: 2016-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport