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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 889ae25405985efae9deccf2120d07a6c9e18562
4
- data.tar.gz: faa849493f9d6e280d370a3e28852e153ac594f3
3
+ metadata.gz: 5e69b46c3904138a9b629c06935167c86d96e121
4
+ data.tar.gz: 869dc7ae697f696c5f65ede168e87b435320e472
5
5
  SHA512:
6
- metadata.gz: f2f705f01edc749d319c9e5a0cdf6eb0689cea3575152e9ba1696138c48a979a4e86d8b5fc5a69079060166162af3bc6efa79219e0ca61f9ff73115bae9aee22
7
- data.tar.gz: 3c711e64c563d5c68e18a7e4490c96ad2889ca2d3c9b2147c5c375bdafa04c72082454e5045551c73513398cb5bf6db7d5975eb482c808f0c2703d14bfc6242b
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 simply `include Granola::Rack` and
55
- you get access to the following interface:
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
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
1
2
  require "granola/version"
2
3
  require "granola/serializer"
3
4
  require "granola/rendering"
4
- require "granola/rack"
5
+ require "granola/util"
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/serializer"
4
- require "granola/util"
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: :json, **opts)
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".freeze] = serializer.last_modified.httpdate
45
+ headers["Last-Modified"] = serializer.last_modified.httpdate
44
46
  end
45
47
 
46
48
  if serializer.cache_key
47
- headers["ETag".freeze] = Digest::MD5.hexdigest(serializer.cache_key)
49
+ headers["ETag"] = Digest::MD5.hexdigest(serializer.cache_key)
48
50
  end
49
51
 
50
- headers["Content-Type".freeze] = serializer.mime_type(as)
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 << serializer.render(as, opts)
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
@@ -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 into JSON.
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
- Object.const_get("%sSerializer" % name)
60
+ constant_lookup.call("%sSerializer" % name)
44
61
  end
45
62
  end
46
63
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Granola
2
- VERSION = "0.11.0"
3
+ VERSION = "0.13.0"
3
4
  end
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.11.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-12 00:00:00.000000000 Z
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
@@ -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