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 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