knuckles 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +40 -13
- data/lib/knuckles/keygen.rb +22 -0
- data/lib/knuckles/pipeline.rb +82 -26
- data/lib/knuckles/stages/combiner.rb +24 -0
- data/lib/knuckles/stages/dumper.rb +23 -0
- data/lib/knuckles/stages/enhancer.rb +21 -0
- data/lib/knuckles/stages/fetcher.rb +30 -0
- data/lib/knuckles/stages/hydrator.rb +29 -0
- data/lib/knuckles/stages/renderer.rb +27 -0
- data/lib/knuckles/stages/writer.rb +41 -0
- data/lib/knuckles/stages.rb +13 -0
- data/lib/knuckles/version.rb +1 -1
- data/lib/knuckles/view.rb +104 -5
- data/lib/knuckles.rb +55 -7
- data/spec/knuckles/pipeline_spec.rb +0 -40
- data/spec/knuckles/{combiner_spec.rb → stages/combiner_spec.rb} +2 -2
- data/spec/knuckles/{dumper_spec.rb → stages/dumper_spec.rb} +2 -2
- data/spec/knuckles/{enhancer_spec.rb → stages/enhancer_spec.rb} +3 -3
- data/spec/knuckles/{fetcher_spec.rb → stages/fetcher_spec.rb} +3 -3
- data/spec/knuckles/{hydrator_spec.rb → stages/hydrator_spec.rb} +3 -3
- data/spec/knuckles/{renderer_spec.rb → stages/renderer_spec.rb} +2 -2
- data/spec/knuckles/{writer_spec.rb → stages/writer_spec.rb} +3 -3
- metadata +31 -43
- data/.gitignore +0 -15
- data/.rspec +0 -2
- data/.rubocop.yml +0 -41
- data/.travis.yml +0 -10
- data/Gemfile +0 -11
- data/Rakefile +0 -9
- data/bench/bench_helper.rb +0 -48
- data/bench/fixtures/serializers.rb +0 -93
- data/bench/fixtures/submissions.json +0 -1
- data/bench/profiling.rb +0 -14
- data/bench/realistic.rb +0 -9
- data/bench/simple.rb +0 -25
- data/bin/rspec +0 -16
- data/knuckles.gemspec +0 -24
- data/lib/knuckles/combiner.rb +0 -26
- data/lib/knuckles/dumper.rb +0 -25
- data/lib/knuckles/enhancer.rb +0 -23
- data/lib/knuckles/fetcher.rb +0 -32
- data/lib/knuckles/hydrator.rb +0 -31
- data/lib/knuckles/renderer.rb +0 -29
- data/lib/knuckles/writer.rb +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac7f83df75c4e52ee59dc85ff6d6d98c863f5963
|
4
|
+
data.tar.gz: e1ff17c2903f39eac251ee3bbaaa56ab1adfc1e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d49c80cd7f0e4b2cefc1861188b84bf9ccd752975d6714d068c8a97798a8e95e615a6192762afe721a6461f9ec23105d66d46651b1cd809fa7c3d6abb577eb8d
|
7
|
+
data.tar.gz: 9e7bd47dd8b17edba6e3e88fa367bca797654b876fc8a4fb2f87f25189bbb503a3c72e8cca288c301ec6c0876e94a5a693ded4ca8a51031bb90e1649b5e80ec3
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## v0.3.0 - 2016-04-07
|
2
|
+
|
3
|
+
* Added: Tons of documentation.
|
4
|
+
* Removed: Remove stage manipulation (`insert_*`, `delete`). All configuration
|
5
|
+
must be done on pipeline init.
|
6
|
+
* Change: Move stage modules into the `Stages` namespace.
|
7
|
+
* Change: Use module names rather than provide a custom name method.
|
8
|
+
|
1
9
|
## v0.2.0 - 2016-03-23
|
2
10
|
|
3
11
|
* Add: Introduce the enhancer stage, used to augment data after it has been
|
data/README.md
CHANGED
@@ -4,7 +4,10 @@
|
|
4
4
|
|
5
5
|
# Knuckles (Because Sonic was Taken)
|
6
6
|
|
7
|
-
|
7
|
+
Knuckles is a performance focused data serialization pipeline. More simply, it
|
8
|
+
tries to serialize models into large JSON payloads as quickly as possible.
|
9
|
+
|
10
|
+
### What's it all about?
|
8
11
|
|
9
12
|
* Emphasis on caching as a composable operation
|
10
13
|
* Reduced object instantiation
|
@@ -13,6 +16,26 @@ What's it all about?
|
|
13
16
|
* Minimal runtime dependencies
|
14
17
|
* Explicit serializer view API with as little overhead and no DSL
|
15
18
|
|
19
|
+
### Is It Better?
|
20
|
+
|
21
|
+
Knuckles is absolutely faster and has a lower memory overhead than uncached
|
22
|
+
or cached usage of `ActiveModelSerializers`, and significantly faster than
|
23
|
+
cached use of `ActiveModelSerializers` with the [perforated][perforated] gem.
|
24
|
+
|
25
|
+
Here are performance and memory comparisons for an endpoint that has been cached
|
26
|
+
with Perforated and with Knuckles. All measurments were done with production
|
27
|
+
settings over the local network.
|
28
|
+
|
29
|
+
| | average | longest | shortest | allocated | retained |
|
30
|
+
| -------------- | ------- | ------- | -------- | --------- | -------- |
|
31
|
+
| perforated/ams | 230ms | 560ms | 190ms | 148,735 | 18,203 |
|
32
|
+
| knuckles/ams | 30ms | 60ms | 20ms | 19,603 | 136 |
|
33
|
+
|
34
|
+
These are measurements for a sizable payload with hundreds of associated
|
35
|
+
records.
|
36
|
+
|
37
|
+
[perforated]: https://github.com/sorentwo/perforated
|
38
|
+
|
16
39
|
## Installation
|
17
40
|
|
18
41
|
Add this line to your application's Gemfile:
|
@@ -46,28 +69,32 @@ end
|
|
46
69
|
With the top level module configured it is simple to jump right into rendering,
|
47
70
|
but we'll look at configuring the pipeline first.
|
48
71
|
|
49
|
-
## Defining Views
|
72
|
+
## Defining Views for Rendering
|
50
73
|
|
51
|
-
|
52
|
-
|
74
|
+
While you can use Knuckles with other serializers, you can also use the provided
|
75
|
+
view layer. Knuckles views are simple templates that let you build up data and
|
76
|
+
relations. They look like this:
|
53
77
|
|
54
78
|
```ruby
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
root 'scout'
|
79
|
+
module ScoutView
|
80
|
+
extend Knuckles::View
|
59
81
|
|
60
|
-
|
82
|
+
def self.root
|
83
|
+
:scouts
|
84
|
+
end
|
61
85
|
|
62
|
-
|
63
|
-
|
86
|
+
def self.data(object, options)
|
87
|
+
{id: object.id, email: object.email, name: object.name}
|
88
|
+
end
|
64
89
|
|
65
|
-
def
|
66
|
-
|
90
|
+
def self.relations(object, options)
|
91
|
+
{things: has_many(object.things, ThingView)}
|
67
92
|
end
|
68
93
|
end
|
69
94
|
```
|
70
95
|
|
96
|
+
See `Knuckles::View` for more usage details.
|
97
|
+
|
71
98
|
## Contributing
|
72
99
|
|
73
100
|
1. Fork it ( https://github.com/sorentwo/knuckles/fork )
|
data/lib/knuckles/keygen.rb
CHANGED
@@ -1,9 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Knuckles
|
4
|
+
# The Keygen module provides simple cache key generation. The global keygen
|
5
|
+
# strategy can be changed at the top level by configuring `Knuckles.keygen`.
|
6
|
+
#
|
7
|
+
# Any object that responds to `expand_key` with a single argument can be
|
8
|
+
# used instead.
|
9
|
+
#
|
10
|
+
# @example Change global keygen stragey
|
11
|
+
#
|
12
|
+
# Knuckles.keygen = MyCustomKeygen.new
|
13
|
+
#
|
4
14
|
module Keygen
|
5
15
|
extend self
|
6
16
|
|
17
|
+
# Calculates a cache key for the given object. It first attempts to use
|
18
|
+
# the object's `cache_key` method (present on `ActiveRecord` models). It
|
19
|
+
# falls back to combining the object's `id` and `updated_at` values.
|
20
|
+
#
|
21
|
+
# @param [Object] object An object to caclculate the key from
|
22
|
+
#
|
23
|
+
# @return [String] Computed cache key
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
#
|
27
|
+
# Knuckles::Keygen.expand_key(model) #=> "MyModel/1/1234567890"
|
28
|
+
#
|
7
29
|
def expand_key(object)
|
8
30
|
if object.respond_to?(:cache_key)
|
9
31
|
object.cache_key
|
data/lib/knuckles/pipeline.rb
CHANGED
@@ -1,39 +1,81 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Knuckles
|
4
|
+
# The `Pipeline` is used to transform a collection of objects into a
|
5
|
+
# serialized result. A pipeline reduces a collection of objects through a set
|
6
|
+
# of stages, passing the result of each stage on to the next. This means that
|
7
|
+
# each stage can act in isolation and can compose with new custom stages.
|
8
|
+
# Additionally, stages are idividually instrumented for performance
|
9
|
+
# monitoring.
|
10
|
+
#
|
11
|
+
# The `Pipeline` class provides a set of default stages but can be overridden
|
12
|
+
# on initialization. Note that after initialization, the stages are frozen to
|
13
|
+
# prevent unpredictable mutation.
|
4
14
|
class Pipeline
|
5
|
-
def self.default_stages
|
6
|
-
[Knuckles::Fetcher,
|
7
|
-
Knuckles::Hydrator,
|
8
|
-
Knuckles::Renderer,
|
9
|
-
Knuckles::Writer,
|
10
|
-
Knuckles::Enhancer,
|
11
|
-
Knuckles::Combiner,
|
12
|
-
Knuckles::Dumper]
|
13
|
-
end
|
14
|
-
|
15
15
|
attr_reader :stages
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
# Creates a new instance of `Knuckles::Pipeline`, optionally with custom
|
18
|
+
# stages.
|
19
|
+
#
|
20
|
+
# @option [Array] :stages (default_stages) An array of stages to pipe
|
21
|
+
# results through
|
22
|
+
#
|
23
|
+
# @example Create a default pipeline
|
24
|
+
#
|
25
|
+
# Knuckles::Pipeline.new
|
26
|
+
#
|
27
|
+
# @example Create a customized pipeline without any caching
|
28
|
+
#
|
29
|
+
# Knuckles::Pipeline.new(stages: [
|
30
|
+
# Knuckles::Stages::Renderer,
|
31
|
+
# Knuckles::Stages::Combiner,
|
32
|
+
# Knuckles::Stages::Dumper
|
33
|
+
# ]
|
34
|
+
def initialize(stages: default_stages)
|
35
|
+
@stages = stages.freeze
|
23
36
|
end
|
24
37
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
38
|
+
# Provides default stage modules in the intended order. These are the
|
39
|
+
# stages that are used if nothing is passed during initialization. The
|
40
|
+
# defaults are defined as a method to make overriding with a subclass easy.
|
41
|
+
#
|
42
|
+
# @example Override `default_stages` within a subclass
|
43
|
+
#
|
44
|
+
# class CustomPipeline < Knuckles::Pipeline
|
45
|
+
# def default_stages
|
46
|
+
# [Knuckles::Stages::Renderer,
|
47
|
+
# Knuckles::Stages::Combiner,
|
48
|
+
# Knuckles::Stages::Dumper]
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
def default_stages
|
53
|
+
[Knuckles::Stages::Fetcher,
|
54
|
+
Knuckles::Stages::Hydrator,
|
55
|
+
Knuckles::Stages::Renderer,
|
56
|
+
Knuckles::Stages::Writer,
|
57
|
+
Knuckles::Stages::Enhancer,
|
58
|
+
Knuckles::Stages::Combiner,
|
59
|
+
Knuckles::Stages::Dumper]
|
35
60
|
end
|
36
61
|
|
62
|
+
# Push a collection of objects through the stages of the pipeline. In
|
63
|
+
# normal usage this will render the objects out to a JSON structure.
|
64
|
+
#
|
65
|
+
# @param [Enumerable] objects A collection of objects (models) to be
|
66
|
+
# serialized and processed.
|
67
|
+
# @param [Hash] options The `call` method doesn't use any options itself,
|
68
|
+
# they are forwarded on to each stage. See the documentation for specific
|
69
|
+
# stages for the options they accept.
|
70
|
+
#
|
71
|
+
# @return [String] The final result as transformed by the pipeline,
|
72
|
+
# typically a JSON string.
|
73
|
+
#
|
74
|
+
# @example Basic pipeline rendering
|
75
|
+
#
|
76
|
+
# pipeline = Knuckles::Pipeline.new
|
77
|
+
# pipeline.call([tag_a, tag_b]) #=> '{"tags":[{"id":1},{"id":2}]}'
|
78
|
+
#
|
37
79
|
def call(objects, options)
|
38
80
|
prepared = prepare(objects)
|
39
81
|
|
@@ -44,6 +86,20 @@ module Knuckles
|
|
44
86
|
end
|
45
87
|
end
|
46
88
|
|
89
|
+
# Convert a collection of objects into a collection of hashes that enclose
|
90
|
+
# the object. The resulting hashes are populated with the keys and default
|
91
|
+
# values necessary for use with the standard pipeline stages.
|
92
|
+
#
|
93
|
+
# @param [Enumerable] objects A collection of objects to prepare
|
94
|
+
#
|
95
|
+
# @return [Array[Hash]] An array of hashes, each with the keys `:object`,
|
96
|
+
# `:key`, `:cached?`, and `:result`.
|
97
|
+
#
|
98
|
+
# @example Prepare a single object
|
99
|
+
#
|
100
|
+
# pipeline.prepare([model]) #=> [{object: model, key: nil,
|
101
|
+
# cached?: false, result: nil}]
|
102
|
+
#
|
47
103
|
def prepare(objects)
|
48
104
|
objects.map do |object|
|
49
105
|
{object: object, key: nil, cached?: false, result: nil}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Knuckles
|
4
|
+
module Stages
|
5
|
+
module Combiner
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def call(prepared, _)
|
9
|
+
prepared.each_with_object(array_backed_hash) do |hash, memo|
|
10
|
+
hash[:result].each do |root, values|
|
11
|
+
case values
|
12
|
+
when Hash then memo[root.to_s] << values
|
13
|
+
when Array then memo[root.to_s] += values
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def array_backed_hash
|
20
|
+
Hash.new { |hash, key| hash[key] = [] }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Knuckles
|
4
|
+
module Stages
|
5
|
+
module Dumper
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def call(objects, _options)
|
9
|
+
Knuckles.serializer.dump(keys_to_arrays(objects))
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def keys_to_arrays(objects)
|
15
|
+
objects.each do |_, value|
|
16
|
+
if value.is_a?(Array)
|
17
|
+
value.uniq! { |hash| hash["id"] || hash[:id] }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Knuckles
|
4
|
+
module Stages
|
5
|
+
module Enhancer
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def call(prepared, options)
|
9
|
+
enhancer = options[:enhancer]
|
10
|
+
|
11
|
+
if enhancer
|
12
|
+
prepared.each do |hash|
|
13
|
+
hash[:result] = enhancer.call(hash[:result], options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
prepared
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Knuckles
|
4
|
+
module Stages
|
5
|
+
module Fetcher
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def call(prepared, options)
|
9
|
+
results = get_cached(prepared, options)
|
10
|
+
|
11
|
+
prepared.each do |hash|
|
12
|
+
result = results[hash[:key]]
|
13
|
+
hash[:cached?] = !result.nil?
|
14
|
+
hash[:result] = result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def get_cached(prepared, options)
|
21
|
+
kgen = options.fetch(:keygen, Knuckles.keygen)
|
22
|
+
keys = prepared.map do |hash|
|
23
|
+
hash[:key] = kgen.expand_key(hash[:object])
|
24
|
+
end
|
25
|
+
|
26
|
+
Knuckles.cache.read_multi(*keys)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Knuckles
|
4
|
+
module Stages
|
5
|
+
module Hydrator
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def call(prepared, options)
|
9
|
+
hydrate = options[:hydrate]
|
10
|
+
|
11
|
+
if hydrate && any_missing?(prepared)
|
12
|
+
hydrate.call(hydratable(prepared))
|
13
|
+
end
|
14
|
+
|
15
|
+
prepared
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def any_missing?(prepared)
|
21
|
+
prepared.any? { |hash| !hash[:cached?] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def hydratable(prepared)
|
25
|
+
prepared.reject { |hash| hash[:cached?] }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Knuckles
|
4
|
+
module Stages
|
5
|
+
module Renderer
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def call(objects, options)
|
9
|
+
view = options.fetch(:view)
|
10
|
+
|
11
|
+
objects.each do |hash|
|
12
|
+
unless hash[:cached?]
|
13
|
+
hash[:result] = do_render(hash[:object], view, options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def do_render(object, view, options)
|
21
|
+
view.relations(object, options).merge!(
|
22
|
+
view.root => [view.data(object, options)]
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Knuckles
|
4
|
+
module Stages
|
5
|
+
module Writer
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def call(objects, _)
|
9
|
+
if cache.respond_to?(:write_multi)
|
10
|
+
write_multi(objects)
|
11
|
+
else
|
12
|
+
write_each(objects)
|
13
|
+
end
|
14
|
+
|
15
|
+
objects
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def cache
|
21
|
+
Knuckles.cache
|
22
|
+
end
|
23
|
+
|
24
|
+
def write_each(objects)
|
25
|
+
objects.each do |hash|
|
26
|
+
cache.write(hash[:key], hash[:result]) unless hash[:cached?]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def write_multi(objects)
|
31
|
+
writable = objects.each_with_object({}) do |hash, memo|
|
32
|
+
next if hash[:cached?]
|
33
|
+
|
34
|
+
memo[hash[:key]] = hash[:result]
|
35
|
+
end
|
36
|
+
|
37
|
+
cache.write_multi(writable) if writable.any?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Knuckles
|
4
|
+
module Stages
|
5
|
+
autoload :Combiner, "knuckles/stages/combiner"
|
6
|
+
autoload :Dumper, "knuckles/stages/dumper"
|
7
|
+
autoload :Enhancer, "knuckles/stages/enhancer"
|
8
|
+
autoload :Fetcher, "knuckles/stages/fetcher"
|
9
|
+
autoload :Hydrator, "knuckles/stages/hydrator"
|
10
|
+
autoload :Renderer, "knuckles/stages/renderer"
|
11
|
+
autoload :Writer, "knuckles/stages/writer"
|
12
|
+
end
|
13
|
+
end
|
data/lib/knuckles/version.rb
CHANGED
data/lib/knuckles/view.rb
CHANGED
@@ -1,28 +1,127 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Knuckles
|
4
|
+
# An absolutely bare bones serializer. It is meant as a replacement for
|
5
|
+
# `ActiveModelSerializers`, but is entirely focused on being simple and
|
6
|
+
# explicit. Views are templates that satisfy the interface of:
|
7
|
+
#
|
8
|
+
# * root #=> symbol
|
9
|
+
# * data #=> hash
|
10
|
+
# * relations #=> hash
|
11
|
+
#
|
12
|
+
# Any object that satisfies that interface can be rendered correctly by the
|
13
|
+
# `Renderer` stage.
|
14
|
+
#
|
15
|
+
# @example Extending for a custom view
|
16
|
+
#
|
17
|
+
# module TagView
|
18
|
+
# extend Knuckles::View
|
19
|
+
#
|
20
|
+
# def root
|
21
|
+
# :tags
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def data(tag, _)
|
25
|
+
# {id: tag.id, name: tag.name}
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# def relations(tag, _)
|
29
|
+
# {posts: has_many(tag.posts, PostView)}
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
4
33
|
module View
|
5
34
|
extend self
|
6
35
|
|
7
|
-
|
8
|
-
|
36
|
+
# Specifies the top level key that data will be stored under. The value
|
37
|
+
# will not be stringified or pluralized during rendering, so be aware of
|
38
|
+
# the format.
|
39
|
+
#
|
40
|
+
# @return [Symbol, nil] By default root is `nil`, it should be overridden to
|
41
|
+
# return a plural symbol.
|
42
|
+
#
|
9
43
|
def root
|
10
44
|
end
|
11
45
|
|
46
|
+
# Serialize an object into a hash. This simply returns an empty hash by
|
47
|
+
# default, it must be overridden by submodules.
|
48
|
+
#
|
49
|
+
# @param [Object] _object The object for serializing.
|
50
|
+
# @param [Hash] _options The options to be used during serialization, i.e.
|
51
|
+
# `:scope`
|
52
|
+
#
|
53
|
+
# @return [Hash] A hash representing the serialized object.
|
54
|
+
#
|
55
|
+
# @example Overriding data
|
56
|
+
#
|
57
|
+
# module TagView
|
58
|
+
# extend Knuckles::View
|
59
|
+
#
|
60
|
+
# def data(tag, _)
|
61
|
+
# {id: tag.id, name: tag.name}
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
12
65
|
def data(_object, _options = {})
|
13
66
|
{}
|
14
67
|
end
|
15
68
|
|
69
|
+
# Extracts associations for an object. Later these are merged with the
|
70
|
+
# output of `data`. View relations are shallow, meaning the relations of
|
71
|
+
# relations are not included.
|
72
|
+
#
|
73
|
+
# @param [Object] _object The object to extract relations from
|
74
|
+
# @param [Hash] _options The options used during extraction, i.e. `:scope`
|
75
|
+
#
|
76
|
+
# @return [Hash] The serialized associations
|
77
|
+
#
|
78
|
+
# @example Override relations
|
79
|
+
#
|
80
|
+
# module PostView
|
81
|
+
# extend Knuckles::View
|
82
|
+
#
|
83
|
+
# def relations(post, _)
|
84
|
+
# {author: has_one(post.author, AuthorView),
|
85
|
+
# comments: has_many(post.comments, CommentView)}
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
#
|
16
89
|
def relations(_object, _options = {})
|
17
90
|
{}
|
18
91
|
end
|
19
92
|
|
20
|
-
|
21
|
-
|
93
|
+
# Renders an associated object using the specified view, wrapping the
|
94
|
+
# results in an array.
|
95
|
+
#
|
96
|
+
# @param [Object] object The associated object to serialize.
|
97
|
+
# @param [Module] view A module responding to `data`.
|
98
|
+
# @param [Hash] options Passed to the view for rendering.
|
99
|
+
#
|
100
|
+
# @return [Array<Hash>] A single rendered data object is always returned.
|
101
|
+
#
|
102
|
+
# @example Render a single association
|
103
|
+
#
|
104
|
+
# PostView.has_one(post.author, AuthorView) #=> [{id: 1, name: "Author"}]
|
105
|
+
#
|
22
106
|
def has_one(object, view, options = {})
|
23
|
-
[
|
107
|
+
has_many([object], view, options)
|
24
108
|
end
|
25
109
|
|
110
|
+
# Renders all associated objects using the specified view.
|
111
|
+
#
|
112
|
+
# @param [Array] objects Array of associated objects to serialize.
|
113
|
+
# @param [Module] view A module responding to `data`.
|
114
|
+
# @param [Hash] options The options passed to the view for rendering.
|
115
|
+
#
|
116
|
+
# @return [Array<Hash>] All rendered association data.
|
117
|
+
#
|
118
|
+
# @example Render a single association
|
119
|
+
#
|
120
|
+
# PostView.has_one(post.authors, AuthorView) #=> [
|
121
|
+
# {id: 1, name: "Me"},
|
122
|
+
# {id: 2, name: "You"}
|
123
|
+
# ]
|
124
|
+
#
|
26
125
|
def has_many(objects, view, options = {})
|
27
126
|
objects.map { |object| view.data(object, options) }
|
28
127
|
end
|