granola 0.11.0 → 0.13.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/README.md +17 -2
- data/lib/granola.rb +2 -1
- data/lib/granola/rack.rb +38 -8
- data/lib/granola/rendering.rb +152 -0
- data/lib/granola/serializer.rb +34 -1
- data/lib/granola/util.rb +19 -2
- data/lib/granola/version.rb +2 -1
- metadata +3 -3
- data/lib/granola/caching.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e69b46c3904138a9b629c06935167c86d96e121
|
4
|
+
data.tar.gz: 869dc7ae697f696c5f65ede168e87b435320e472
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00b2b5b36f45cc56c884b9eaabc02aa8aef1b65cfb30480f54cf9568d3f12d992170b5961226c1b368ad60fd20e1ee30de68866a588149c1015954b74ccd884e
|
7
|
+
data.tar.gz: 3ad2e3746539fc912d747ed414d5aaca1498e4ff0fe35f35c3913ee75afe61edfe5659e65648999b4a62283b0ee1f7f1d004b37df8699a11f78370b7b786ccd4
|
data/README.md
CHANGED
@@ -51,8 +51,9 @@ serializer.to_json #=> '[{"name":"John Doe",...},{...}]'
|
|
51
51
|
|
52
52
|
## Rack Helpers
|
53
53
|
|
54
|
-
If your application is based on Rack, you can
|
55
|
-
|
54
|
+
If your application is based on Rack, you can `require "granola/rack"` instead
|
55
|
+
of `require "granola"`, and then simply `include Granola::Rack` to get access
|
56
|
+
to the following interface:
|
56
57
|
|
57
58
|
``` ruby
|
58
59
|
granola(person) #=> This will infer PersonSerializer from a Person instance
|
@@ -63,6 +64,8 @@ This method returns a Rack response tuple that you can use like so (this example
|
|
63
64
|
uses [Cuba][], but similar code will work for other frameworks):
|
64
65
|
|
65
66
|
``` ruby
|
67
|
+
require "granola/rack"
|
68
|
+
|
66
69
|
Cuba.plugin Granola::Rack
|
67
70
|
|
68
71
|
Cuba.define do
|
@@ -142,6 +145,18 @@ granola(object, as: :msgpack)
|
|
142
145
|
|
143
146
|
This will set the correct MIME type.
|
144
147
|
|
148
|
+
If you don't explicitly set a format when rendering via the rack helper, Granola
|
149
|
+
will use the request's `Accept` header to choose the best format for rendering.
|
150
|
+
|
151
|
+
For example, given a request with the following header:
|
152
|
+
|
153
|
+
Accept: text/x-yaml;q=0.5,application/x-msgpack;q=0.8,*/*;q=0.2
|
154
|
+
|
155
|
+
Granola will check first if you have a renderer registered for the
|
156
|
+
`application/x-msgpack` MIME type, and then check for a renderer for the
|
157
|
+
`text/x-yaml` MIME type. If none of these are registered, it will default to
|
158
|
+
rendering JSON.
|
159
|
+
|
145
160
|
[msgpack-ruby]: https://github.com/msgpack/msgpack-ruby
|
146
161
|
|
147
162
|
## License
|
data/lib/granola.rb
CHANGED
data/lib/granola/rack.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "digest/md5"
|
2
3
|
require "time"
|
3
|
-
require "granola
|
4
|
-
require "
|
5
|
-
require "granola/caching"
|
4
|
+
require "granola"
|
5
|
+
require "rack/utils"
|
6
6
|
|
7
7
|
module Granola
|
8
8
|
# Mixin to render JSON in the context of a Rack application. See the #json
|
@@ -29,6 +29,8 @@ module Granola
|
|
29
29
|
# example, MsgPack.
|
30
30
|
# status: The HTTP status to return on stale responses. Defaults to `200`
|
31
31
|
# headers: A Hash of default HTTP headers. Defaults to an empty Hash.
|
32
|
+
# env: Rack's env-Hash to inspect request headers. Defaults to
|
33
|
+
# an `env` method in the object that includes `Granola::Rack`.
|
32
34
|
# **opts: Any other keywords passed will be forwarded to the serializer's
|
33
35
|
# serialization backend call.
|
34
36
|
#
|
@@ -36,24 +38,52 @@ module Granola
|
|
36
38
|
# infer one for this object.
|
37
39
|
#
|
38
40
|
# Returns a Rack response tuple.
|
39
|
-
def granola(object, with: nil, status: 200, headers: {}, as: :
|
41
|
+
def granola(object, with: nil, status: 200, headers: {}, as: nil, env: self.env, **opts)
|
40
42
|
serializer = Granola::Util.serializer_for(object, with: with)
|
41
43
|
|
42
44
|
if serializer.last_modified
|
43
|
-
headers["Last-Modified"
|
45
|
+
headers["Last-Modified"] = serializer.last_modified.httpdate
|
44
46
|
end
|
45
47
|
|
46
48
|
if serializer.cache_key
|
47
|
-
headers["ETag"
|
49
|
+
headers["ETag"] = Digest::MD5.hexdigest(serializer.cache_key)
|
48
50
|
end
|
49
51
|
|
50
|
-
|
52
|
+
renderer = Granola.renderer(
|
53
|
+
as || Granola::Rack.best_format_for(env["HTTP_ACCEPT"]) || :json
|
54
|
+
)
|
55
|
+
|
56
|
+
headers["Content-Type"] = renderer.content_type
|
51
57
|
|
52
58
|
body = Enumerator.new do |yielder|
|
53
|
-
yielder <<
|
59
|
+
yielder << renderer.render(serializer, opts)
|
54
60
|
end
|
55
61
|
|
56
62
|
[status, headers, body]
|
57
63
|
end
|
64
|
+
|
65
|
+
# Internal: Infer the best rendering format based on the value of an Accept
|
66
|
+
# header and the available registered renderers.
|
67
|
+
#
|
68
|
+
# If there are renderers registered for JSON, and MessagePack, an Accept
|
69
|
+
# header preferring MessagePack over JSON will result in the response being
|
70
|
+
# serialized into MessagePack instead of JSON, unless the user explicitly
|
71
|
+
# prefers JSON.
|
72
|
+
#
|
73
|
+
# accept - String with the value of an HTTP Accept header.
|
74
|
+
# formats - Map of registered renderers. Defaults to
|
75
|
+
# `Granola.renderable_formats`.
|
76
|
+
#
|
77
|
+
# Returns a Symbol with a Renderer type, or `nil` if none could be inferred.
|
78
|
+
def self.best_format_for(accept, formats = Granola.renderable_formats)
|
79
|
+
formats = formats.map { |f| [Granola.renderer(f).content_type, f] }.to_h
|
80
|
+
|
81
|
+
::Rack::Utils.q_values(accept).sort_by { |_, q| -q }.each do |type, _|
|
82
|
+
format = formats[type]
|
83
|
+
return format unless format.nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
nil
|
87
|
+
end
|
58
88
|
end
|
59
89
|
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Granola
|
5
|
+
class NoSuchRendererError < ArgumentError; end
|
6
|
+
|
7
|
+
# Public: Register a new Renderer. A Renderer must be a callable that, given
|
8
|
+
# the output of a Serializer's `data` method, will turn that into a stream
|
9
|
+
# of the expected type.
|
10
|
+
#
|
11
|
+
# Registering a Renderer via this method will add a `to_#{type}` instance
|
12
|
+
# method to the serializers, as syntax sugar for calling `Renderer#render`.
|
13
|
+
#
|
14
|
+
# type - A name to identify this rendering mechanism. For example,
|
15
|
+
# `:json`.
|
16
|
+
# via: - A callable that performs the actual rendering. See
|
17
|
+
# Renderer#initialize for a description of the interface
|
18
|
+
# expected of this callable.
|
19
|
+
# content_type: - String with the expected content type when rendering
|
20
|
+
# serializers in a web context. For example,
|
21
|
+
# `"application/json"`
|
22
|
+
#
|
23
|
+
# Example:
|
24
|
+
#
|
25
|
+
# Granola::Serializer.render(:json, {
|
26
|
+
# via: Oj.method(:dump),
|
27
|
+
# content_type: "application/json"
|
28
|
+
# })
|
29
|
+
#
|
30
|
+
# Granola::Serializer.render(:msgpack, {
|
31
|
+
# via: MessagePack.method(:pack),
|
32
|
+
# content_type: "application/x-msgpack"
|
33
|
+
# })
|
34
|
+
#
|
35
|
+
# Returns nothing.
|
36
|
+
def self.render(type, via:, content_type:)
|
37
|
+
RENDERERS[type.to_sym] = Renderer.new(via, content_type)
|
38
|
+
Serializer.send :define_method, "to_#{type}" do |**opts, &block|
|
39
|
+
Granola.renderer(type).render(self, **opts, &block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Public: Get a registered Renderer.
|
44
|
+
#
|
45
|
+
# type - A Symbol with the name under which a Renderer was registered. See
|
46
|
+
# `Granola.render`.
|
47
|
+
#
|
48
|
+
# Raises Granola::NoSuchRendererError if an unknown `type` is passed.
|
49
|
+
# Returns a Granola::Renderer instance.
|
50
|
+
def self.renderer(type)
|
51
|
+
RENDERERS.fetch(type.to_sym) do
|
52
|
+
fail NoSuchRendererError, "No renderer registered for #{type.inspect}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Returns an Array of types with a registered Renderer as Symbols. See
|
57
|
+
# `Granola.render` to register a new Renderer instance.
|
58
|
+
def self.renderable_formats
|
59
|
+
RENDERERS.keys
|
60
|
+
end
|
61
|
+
|
62
|
+
# Internal: Map of renderers available to this serializer.
|
63
|
+
RENDERERS = {}
|
64
|
+
|
65
|
+
# Renderer objects just wrap the callable used to render an object and keep a
|
66
|
+
# reference to the Content-Type that should be used when rendering objects
|
67
|
+
# through it.
|
68
|
+
#
|
69
|
+
# You shouldn't initialize this objects. Instead, use `Granola.render` to
|
70
|
+
# register rendering formats.
|
71
|
+
#
|
72
|
+
# Example:
|
73
|
+
#
|
74
|
+
# Granola.render(:json, via: JSON.method(:generate),
|
75
|
+
# content_type: "application/json")
|
76
|
+
#
|
77
|
+
# renderer = Granola.renderer(:json)
|
78
|
+
# renderer.content_type #=> "application/json"
|
79
|
+
# renderer.render(PersonSerializer.new(person)) #=> "{...}"
|
80
|
+
class Renderer
|
81
|
+
# Public: Get a String with the Renderer's expected content type.
|
82
|
+
attr_reader :content_type
|
83
|
+
|
84
|
+
# Internal: Initialize a renderer. You should't use this method. Instead,
|
85
|
+
# see `Granola.render` for registering a new rendering format.
|
86
|
+
#
|
87
|
+
# backend - A callable that takes an object (the result of calling a
|
88
|
+
# Serializer's `data` method) and returns a String of data
|
89
|
+
# appropriately encoded for this rendering format.
|
90
|
+
# content_type - A String with the expected content type to be returned when
|
91
|
+
# serializing objects through this renderer.
|
92
|
+
def initialize(backend, content_type)
|
93
|
+
@backend = backend
|
94
|
+
@content_type = content_type
|
95
|
+
end
|
96
|
+
|
97
|
+
# Public: Render a Serializer in this format.
|
98
|
+
#
|
99
|
+
# serializer - An instance of Granola::Serializer.
|
100
|
+
# options - Any options that can be passed to the rendering backend.
|
101
|
+
#
|
102
|
+
# Returns a String.
|
103
|
+
def render(serializer, **options, &block)
|
104
|
+
@backend.call(serializer.data, **options, &block)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
render :json, via: JSON.method(:generate), content_type: "application/json"
|
109
|
+
|
110
|
+
# Deprecated: The old way of registering a JSON renderer. This will be gone in
|
111
|
+
# 1.0.
|
112
|
+
#
|
113
|
+
# callable - A callable. See the format in `Granola.render`
|
114
|
+
#
|
115
|
+
# Returns nothing.
|
116
|
+
def self.json=(callable)
|
117
|
+
warn "Granola.json= has been deprecated. Use Granola.render now."
|
118
|
+
render(:json, via: callable, content_type: "application/json")
|
119
|
+
end
|
120
|
+
|
121
|
+
class Serializer
|
122
|
+
# Deprecated: Use Granola.renderer and a Renderer's render method directly.
|
123
|
+
# This method will be removed in 1.0.
|
124
|
+
#
|
125
|
+
# type - A Symbol with the expected rendering format.
|
126
|
+
# **options - An options Hash or set of keyword arguments that will be
|
127
|
+
# passed to the renderer.
|
128
|
+
#
|
129
|
+
# Raises KeyError if there's no Renderer registered for the given `type`.
|
130
|
+
# Returns a String (in the encoding approrpriate to the rendering format.)
|
131
|
+
def render(type = :json, **options, &block)
|
132
|
+
warn "Granola::Serializer#render has been deprecated. Use Granola.renderer(type).render."
|
133
|
+
Granola.renderer(type).render(self, **options, &block)
|
134
|
+
rescue NoSuchRendererError => err
|
135
|
+
fail KeyError, err.message
|
136
|
+
end
|
137
|
+
|
138
|
+
# Deprecated: Use Granola.renderer and the Renderer's content_type method
|
139
|
+
# directly. This method will be removed in 1.0.
|
140
|
+
#
|
141
|
+
# type - A Symbol describing the expected rendering format.
|
142
|
+
#
|
143
|
+
# Raises KeyError if there's no Renderer registered for the given `type`.
|
144
|
+
# Returns a String.
|
145
|
+
def mime_type(type = :json)
|
146
|
+
warn "Granola::Serializer#mime_type has been deprecated. Use Granola.renderer(type).content_type."
|
147
|
+
Granola.renderer(type).content_type
|
148
|
+
rescue NoSuchRendererError => err
|
149
|
+
fail KeyError, err.message
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/lib/granola/serializer.rb
CHANGED
@@ -2,6 +2,7 @@ module Granola
|
|
2
2
|
# A Serializer describes how to serialize a certain type of object, by
|
3
3
|
# declaring the structure of JSON objects.
|
4
4
|
class Serializer
|
5
|
+
# Public: Get the domain model to serialize.
|
5
6
|
attr_reader :object
|
6
7
|
|
7
8
|
# Public: Instantiates a list serializer that wraps around an iterable of
|
@@ -19,7 +20,7 @@ module Granola
|
|
19
20
|
|
20
21
|
# Public: Initialize the serializer with a given object.
|
21
22
|
#
|
22
|
-
# object - The domain model that we want to serialize
|
23
|
+
# object - The domain model that we want to serialize.
|
23
24
|
def initialize(object)
|
24
25
|
@object = object
|
25
26
|
end
|
@@ -33,6 +34,22 @@ module Granola
|
|
33
34
|
def data
|
34
35
|
fail NotImplementedError
|
35
36
|
end
|
37
|
+
|
38
|
+
# Public: Provides a key that's unique to the current representation of the
|
39
|
+
# JSON object generated by the serializer. This will be MD5'd to become the
|
40
|
+
# ETag header that will be sent in responses.
|
41
|
+
#
|
42
|
+
# Returns a String or `nil`, indicaing that no ETag should be sent.
|
43
|
+
def cache_key
|
44
|
+
end
|
45
|
+
|
46
|
+
# Public: Provides the date of last modification of this entity. This will
|
47
|
+
# become the Last-Modified header that will be sent in responses, if
|
48
|
+
# present.
|
49
|
+
#
|
50
|
+
# Returns a Time or `nil`, indicating that no Last-Modified should be sent.
|
51
|
+
def last_modified
|
52
|
+
end
|
36
53
|
end
|
37
54
|
|
38
55
|
# Internal: The List serializer provides an interface for serializing lists of
|
@@ -66,5 +83,21 @@ module Granola
|
|
66
83
|
def data
|
67
84
|
@list.map(&:data)
|
68
85
|
end
|
86
|
+
|
87
|
+
# Public: Defines a compound cache key by joining the cache key of any
|
88
|
+
# items of the collection, if they have one. See `Serializer#cache_key`.
|
89
|
+
#
|
90
|
+
# Returns a String or `nil`.
|
91
|
+
def cache_key
|
92
|
+
all = @list.map(&:cache_key).compact
|
93
|
+
all.join("-") if all.any?
|
94
|
+
end
|
95
|
+
|
96
|
+
# Public: Returns the newest modification Time out of the items in the
|
97
|
+
# collection, or `nil` if none have a `last_modified` set. See
|
98
|
+
# `Serializer#last_modified`.
|
99
|
+
def last_modified
|
100
|
+
@list.map(&:last_modified).compact.max
|
101
|
+
end
|
69
102
|
end
|
70
103
|
end
|
data/lib/granola/util.rb
CHANGED
@@ -1,7 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "granola/serializer"
|
2
3
|
|
3
4
|
module Granola
|
4
5
|
module Util
|
6
|
+
class << self
|
7
|
+
# Public: Get/Set the mechanism used to look up constants by name. This
|
8
|
+
# should be a callable that takes a String with a constant's name and
|
9
|
+
# converts this to a Serializer class, or fails with NameError.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# # Find serializers under Serializers::Foo instead of FooSerializer.
|
14
|
+
# Granola::Util.constant_lookup = ->(name) do
|
15
|
+
# Serializers.const_get(name.sub(/Serializer$/, ""))
|
16
|
+
# end
|
17
|
+
attr_accessor :constant_lookup
|
18
|
+
end
|
19
|
+
|
20
|
+
self.constant_lookup = Object.method(:const_get)
|
21
|
+
|
5
22
|
# Public: Returns the serializer object for rendering a specific object. The
|
6
23
|
# class will attempt to be inferred based on the class of the passed object,
|
7
24
|
# but a specific serializer can be passed via a keyword argument `with`.
|
@@ -36,11 +53,11 @@ module Granola
|
|
36
53
|
object = object.respond_to?(:to_ary) ? object.to_ary[0] : object
|
37
54
|
|
38
55
|
case object
|
39
|
-
when NilClass, TrueClass, FalseClass, Numeric, String
|
56
|
+
when Hash, NilClass, TrueClass, FalseClass, Numeric, String
|
40
57
|
PrimitiveTypesSerializer
|
41
58
|
else
|
42
59
|
name = object.class.name
|
43
|
-
|
60
|
+
constant_lookup.call("%sSerializer" % name)
|
44
61
|
end
|
45
62
|
end
|
46
63
|
|
data/lib/granola/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: granola
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicolas Sanguinetti
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-11-
|
11
|
+
date: 2016-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cutest
|
@@ -48,8 +48,8 @@ files:
|
|
48
48
|
- LICENSE
|
49
49
|
- README.md
|
50
50
|
- lib/granola.rb
|
51
|
-
- lib/granola/caching.rb
|
52
51
|
- lib/granola/rack.rb
|
52
|
+
- lib/granola/rendering.rb
|
53
53
|
- lib/granola/serializer.rb
|
54
54
|
- lib/granola/util.rb
|
55
55
|
- lib/granola/version.rb
|
data/lib/granola/caching.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
require "granola/serializer"
|
2
|
-
|
3
|
-
module Granola
|
4
|
-
# Mixin to add caching-awareness to Serializers.
|
5
|
-
module Caching
|
6
|
-
# Public: Provides a key that's unique to the current representation of the
|
7
|
-
# JSON object generated by the serializer. This will be MD5'd to become the
|
8
|
-
# ETag header that will be sent in responses.
|
9
|
-
#
|
10
|
-
# Returns a String or `nil`, indicaing that no ETag should be sent.
|
11
|
-
def cache_key
|
12
|
-
end
|
13
|
-
|
14
|
-
# Public: Provides the date of last modification of this entity. This will
|
15
|
-
# become the Last-Modified header that will be sent in responses, if
|
16
|
-
# present.
|
17
|
-
#
|
18
|
-
# Returns a Time or `nil`, indicating that no Last-Modified should be sent.
|
19
|
-
def last_modified
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
class Serializer
|
24
|
-
include Caching
|
25
|
-
end
|
26
|
-
|
27
|
-
class List < Serializer
|
28
|
-
def cache_key
|
29
|
-
all = @list.map(&:cache_key).compact
|
30
|
-
all.join("-") if all.any?
|
31
|
-
end
|
32
|
-
|
33
|
-
def last_modified
|
34
|
-
@list.map(&:last_modified).compact.sort.last
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|