granola 0.11.0 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|