knuckles 0.4.0 → 0.5.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
  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