granola 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +87 -0
- data/lib/granola/caching.rb +37 -0
- data/lib/granola/helper.rb +44 -0
- data/lib/granola/rack.rb +128 -0
- data/lib/granola/version.rb +3 -0
- data/lib/granola.rb +84 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9d89e36c1e6cf872c3e3fafff7ddabd364af6a20
|
4
|
+
data.tar.gz: 292659322f51f57ba16ad7a47cda7aa366ce2362
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6a5f68618ec9682f837db0a45b033bf850dc256875d3b26fcad3b187a26401bcd0bc569fc3512ab63e29781a7e2215838670de4695b31fbc568a3251276d761a
|
7
|
+
data.tar.gz: e64ee844b50f5ee5b2081988406bf408f9b3bd51e35e406795b3c188b078c32a3a0b2dd8059df701471de60b9e094fa436f54b4fee87d2d787f50dbb4cc17f11
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Nicolas Sanguinetti <hi@nicolassanguinetti.info>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Granola, a JSON serializer
|
2
|
+
|
3
|
+
Granola aims to provide a simple interface to generate JSON responses based on
|
4
|
+
your application's domain models. It doesn't make assumptions about anything and
|
5
|
+
gets out of your way. You just write plain ruby.
|
6
|
+
|
7
|
+
## Example
|
8
|
+
|
9
|
+
``` ruby
|
10
|
+
class PersonSerializer < Granola::Serializer
|
11
|
+
def attributes
|
12
|
+
{
|
13
|
+
"name" => object.name,
|
14
|
+
"email" => object.email,
|
15
|
+
"age" => object.age
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
PersonSerializer.new(person).to_json #=> '{"name":"John Doe",...}'
|
21
|
+
```
|
22
|
+
|
23
|
+
## Install
|
24
|
+
|
25
|
+
gem install granola
|
26
|
+
|
27
|
+
## JSON serialization
|
28
|
+
|
29
|
+
Granola doesn't make assumptions about your code, so it shouldn't depend on a
|
30
|
+
specific JSON backend. It uses [MultiJson][] to serialize your objects with your
|
31
|
+
favorite backend.
|
32
|
+
|
33
|
+
Try to avoid using the default, which is the `stdlib`'s pure-ruby JSON library,
|
34
|
+
since it's slow. If in doubt, I like [Yajl][].
|
35
|
+
|
36
|
+
If you want to pass options to `MultiJson` (like `pretty: true`), any keywords
|
37
|
+
passed to `#to_json` will be forwarded to `MultiJson.dump`.
|
38
|
+
|
39
|
+
[MultiJson]: https://github.com/intridea/multi_json
|
40
|
+
[Yajl]: https://github.com/brianmario/yajl-ruby
|
41
|
+
|
42
|
+
## Handling lists of models
|
43
|
+
|
44
|
+
A Granola serializer can handle a list of entities of the same type by using the
|
45
|
+
`Serializer.list` method (instead of `Serializer.new`). For example:
|
46
|
+
|
47
|
+
``` ruby
|
48
|
+
serializer = PersonSerializer.list(Person.all)
|
49
|
+
serializer.to_json #=> '[{"name":"John Doe",...},{...}]'
|
50
|
+
```
|
51
|
+
|
52
|
+
## Rack Helpers
|
53
|
+
|
54
|
+
If your application is based on rack, you have a `Rack::Response` called `res`
|
55
|
+
in your context, and you have an `env` method that returns the Rack env hash,
|
56
|
+
you can simply `include Granola::Rack` and you get access to the following
|
57
|
+
interface:
|
58
|
+
|
59
|
+
``` ruby
|
60
|
+
json(person) #=> This will try to infer PersonSerializer from a Person instance
|
61
|
+
json(person, with: AnotherSerializer)
|
62
|
+
```
|
63
|
+
|
64
|
+
*NOTE*: This works out of the box in frameworks like [Cuba][] or [Roda][]. You
|
65
|
+
might need to provide glue code to make `res` and/or `env` available in other
|
66
|
+
frameworks.
|
67
|
+
|
68
|
+
[Cuba]: https://github.com/soveran/cuba
|
69
|
+
[Roda]: https://github.com/jeremyevans/roda
|
70
|
+
|
71
|
+
## Caching
|
72
|
+
|
73
|
+
`Granola::Serializer` gives you to methods that you can implement in your
|
74
|
+
serializers, `last_modified` and `cache_key`, that will be used to prevent
|
75
|
+
rendering JSON at all if possible, when using the `Granola::Rack#json` helper.
|
76
|
+
|
77
|
+
If your serializer implements this method, and the `env` has the appropriate
|
78
|
+
`If-Modified-Since` or `If-None-Match` headers, the helper will automatically
|
79
|
+
return a 304 response.
|
80
|
+
|
81
|
+
Plus, it sets appropriate `ETag` and `Last-Modified` so your clients can avoid
|
82
|
+
hitting the endpoint altogether.
|
83
|
+
|
84
|
+
## License
|
85
|
+
|
86
|
+
This project is shared under the MIT license. See the attached LICENSE file for
|
87
|
+
details.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "granola"
|
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
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "granola"
|
2
|
+
|
3
|
+
module Granola::Helper
|
4
|
+
# Public: Returns the serializer object for rendering a specific object. The
|
5
|
+
# class will attempt to be inferred based on the class of the passed object,
|
6
|
+
# but a specific serializer can be passed via a keyword argument `with`.
|
7
|
+
#
|
8
|
+
# object - The Object to serialize.
|
9
|
+
#
|
10
|
+
# Keywords
|
11
|
+
# with: A specific serializer class to use. If this is `nil`,
|
12
|
+
# `Helper.serializer_class_for` will be used to infer the serializer
|
13
|
+
# class.
|
14
|
+
#
|
15
|
+
# Raises NameError if no specific serializer is provided and we fail to infer
|
16
|
+
# one for this object.
|
17
|
+
# Returns an instance of a Granola::Serializer subclass.
|
18
|
+
def serializer_for(object, with: nil)
|
19
|
+
serializer_class = with || Granola::Helper.serializer_class_for(object)
|
20
|
+
method = object.respond_to?(:to_ary) ? :list : :new
|
21
|
+
serializer_class.send(method, object)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Internal: Infers the name of a serialized based on the class of the passed
|
25
|
+
# object. The pattern is the Object's class + "Serializer". So
|
26
|
+
# `PersonSerializer` for `Person`.
|
27
|
+
#
|
28
|
+
# object - An object of a class with a matching serializer.
|
29
|
+
#
|
30
|
+
# Raises NameError if no matching class exists.
|
31
|
+
# Returns a Class.
|
32
|
+
def self.serializer_class_for(object)
|
33
|
+
object = object.respond_to?(:to_ary) ? object.to_ary.fetch(0, nil) : object
|
34
|
+
const_get("#{object.class.name}Serializer")
|
35
|
+
end
|
36
|
+
|
37
|
+
# Internal: Null serializer that transparently handles rendering `nil` in case
|
38
|
+
# it's passed.
|
39
|
+
class NilClassSerializer < Granola::Serializer
|
40
|
+
def attributes
|
41
|
+
{}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/granola/rack.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require "digest/md5"
|
2
|
+
require "time"
|
3
|
+
require "granola"
|
4
|
+
require "granola/helper"
|
5
|
+
require "granola/caching"
|
6
|
+
|
7
|
+
# Mixin to render JSON in the context of a Rack application. See the #json
|
8
|
+
# method for the specifics.
|
9
|
+
module Granola::Rack
|
10
|
+
def self.included(base)
|
11
|
+
base.send(:include, Granola::Helper)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Public: Renders a JSON representation of an object using a
|
15
|
+
# Granola::Serializer. This takes care of setting the `Last-Modified` and
|
16
|
+
# `ETag` headers if appropriate, and of controlling whether the object should
|
17
|
+
# be rendered at all or not (issuing a 304 response in this case).
|
18
|
+
#
|
19
|
+
# This expects the class mixing in this module to implement two methods:
|
20
|
+
# `res`, that should be a Rack::Response, and `env`, that should be a Rack
|
21
|
+
# environment hash.
|
22
|
+
#
|
23
|
+
# object - An object to serialize into JSON.
|
24
|
+
#
|
25
|
+
# Keywords:
|
26
|
+
# with: A specific serializer class to use. If this is `nil`,
|
27
|
+
# `Helper.serializer_class_for` will be used to infer the
|
28
|
+
# serializer class.
|
29
|
+
# **json_options: Any other keywords passed will be forwarded to the
|
30
|
+
# serializer's `#to_json` call.
|
31
|
+
#
|
32
|
+
# Raises NameError if no specific serializer is provided and we fail to infer
|
33
|
+
# one for this object.
|
34
|
+
# Returns an instance of a Granola::Serializer subclass.
|
35
|
+
def json(object, with: nil, **json_options)
|
36
|
+
serializer = serializer_for(object, with: with)
|
37
|
+
|
38
|
+
if serializer.last_modified
|
39
|
+
res["Last-Modified".freeze] = serializer.last_modified.httpdate
|
40
|
+
end
|
41
|
+
|
42
|
+
if serializer.cache_key
|
43
|
+
res["ETag".freeze] = Digest::MD5.hexdigest(serializer.cache_key)
|
44
|
+
end
|
45
|
+
|
46
|
+
stale_check = StaleCheck.new(
|
47
|
+
env, last_modified: serializer.last_modified, etag: serializer.cache_key
|
48
|
+
)
|
49
|
+
|
50
|
+
if stale_check.fresh?
|
51
|
+
res.status = 304
|
52
|
+
else
|
53
|
+
json_string = serializer.to_json(json_options)
|
54
|
+
res["Content-Type".freeze] = serializer.mime_type
|
55
|
+
res["Content-Length".freeze] = json_string.length.to_s
|
56
|
+
res.write(json_string)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Internal: Check whether a request is fresh or stale by both modified time
|
61
|
+
# and/or etag.
|
62
|
+
class StaleCheck
|
63
|
+
# Internal: Get the env Hash of the request.
|
64
|
+
attr_reader :env
|
65
|
+
|
66
|
+
# Internal: Get the Time at which the domain model was last-modified.
|
67
|
+
attr_reader :last_modified
|
68
|
+
|
69
|
+
# Internal: Get the String with the ETag ggenerated by this domain model.
|
70
|
+
attr_reader :etag
|
71
|
+
|
72
|
+
IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE".freeze
|
73
|
+
IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze
|
74
|
+
|
75
|
+
# Public: Initialize the check.
|
76
|
+
#
|
77
|
+
# env - Rack's env Hash.
|
78
|
+
#
|
79
|
+
# Keywords:
|
80
|
+
# last_modified: The Time at which the domain model was last modified, if
|
81
|
+
# applicable (Defaults to `nil`).
|
82
|
+
# etag: The HTTP ETag for this domain model, if applicable
|
83
|
+
# (Defaults to `nil`).
|
84
|
+
def initialize(env, last_modified: nil, etag: nil)
|
85
|
+
@env = env
|
86
|
+
@last_modified = last_modified
|
87
|
+
@etag = etag
|
88
|
+
end
|
89
|
+
|
90
|
+
# Public: Checks whether the request is fresh. A fresh request is one that
|
91
|
+
# is stored in the client's cache and doesn't need updating (so it can be
|
92
|
+
# responded to with a 304 response).
|
93
|
+
#
|
94
|
+
# Returns Boolean.
|
95
|
+
def fresh?
|
96
|
+
fresh_by_time? || fresh_by_etag?
|
97
|
+
end
|
98
|
+
|
99
|
+
# Public: Returns a Boolean denoting whether the request is stale (i.e. not
|
100
|
+
# fresh).
|
101
|
+
def stale?
|
102
|
+
!fresh?
|
103
|
+
end
|
104
|
+
|
105
|
+
# Internal: Checks if a request is fresh by modified time, if applicable, by
|
106
|
+
# comparing the `If-Modified-Since` header with the last modified time of
|
107
|
+
# the domain model.
|
108
|
+
#
|
109
|
+
# Returns Boolean.
|
110
|
+
def fresh_by_time?
|
111
|
+
return false unless env.key?(IF_MODIFIED_SINCE) && !last_modified.nil?
|
112
|
+
if_modified_since = Time.parse(env.fetch(IF_MODIFIED_SINCE))
|
113
|
+
last_modified <= if_modified_since
|
114
|
+
end
|
115
|
+
|
116
|
+
# Internal: Checks if a request is fresh by etag, if applicable, by
|
117
|
+
# comparing the `If-None-Match` header with the ETag for the domain model.
|
118
|
+
#
|
119
|
+
# Returns Boolean.
|
120
|
+
def fresh_by_etag?
|
121
|
+
return false unless env.key?(IF_NONE_MATCH) && !etag.nil?
|
122
|
+
if_none_match = env.fetch(IF_NONE_MATCH, "").split(/\s*,\s*/)
|
123
|
+
return false if if_none_match.empty?
|
124
|
+
return true if if_none_match.include?("*".freeze)
|
125
|
+
if_none_match.any? { |tag| tag == etag }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/granola.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require "multi_json"
|
2
|
+
require "granola/version"
|
3
|
+
|
4
|
+
module Granola
|
5
|
+
# A Serializer describes how to serialize a certain type of object, by
|
6
|
+
# declaring the structure of JSON objects.
|
7
|
+
class Serializer
|
8
|
+
attr_reader :object
|
9
|
+
|
10
|
+
# Public: Instantiates a list serializer that wraps around an iterable of
|
11
|
+
# objects of the type expected by this serializer class.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# serializer = PersonSerializer.list(people)
|
16
|
+
# serializer.to_json
|
17
|
+
#
|
18
|
+
# Returns a Granola::List.
|
19
|
+
def self.list(ary)
|
20
|
+
List.new(ary, self)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Public: Initialize the serializer with a given object.
|
24
|
+
#
|
25
|
+
# object - The domain model that we want to serialize into JSON.
|
26
|
+
def initialize(object)
|
27
|
+
@object = object
|
28
|
+
end
|
29
|
+
|
30
|
+
# Public: Returns the Hash of attributes that should be serialized into
|
31
|
+
# JSON.
|
32
|
+
#
|
33
|
+
# Raises NotImplementedError unless you override in subclasses.
|
34
|
+
def attributes
|
35
|
+
fail NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Generate the JSON string using the current MultiJson adapter.
|
39
|
+
#
|
40
|
+
# **options - Any options valid for `MultiJson.dump`.
|
41
|
+
#
|
42
|
+
# Returns a String.
|
43
|
+
def to_json(**options)
|
44
|
+
MultiJson.dump(attributes, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Returns the MIME type generated by this serializer. By default
|
48
|
+
# this will be `application/json`, but you can override in your serializers
|
49
|
+
# if your API uses a different MIME type (e.g. `application/my-app+json`).
|
50
|
+
#
|
51
|
+
# Returns a String.
|
52
|
+
def mime_type
|
53
|
+
"application/json".freeze
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Internal: The List serializer provides an interface for serializing lists of
|
58
|
+
# objects, wrapping around a specific serializer.
|
59
|
+
#
|
60
|
+
# Example:
|
61
|
+
#
|
62
|
+
# serializer = Granola::List.new(people, PersonSerializer)
|
63
|
+
# serializer.to_json
|
64
|
+
#
|
65
|
+
# You should use Serializer.list instead of this class.
|
66
|
+
class List < Serializer
|
67
|
+
# Internal: Get the serializer class to use for each item of the list.
|
68
|
+
attr_reader :item_serializer
|
69
|
+
|
70
|
+
# Public: Instantiate a new list serializer.
|
71
|
+
#
|
72
|
+
# list - An Array-like structure.
|
73
|
+
# serializer - A subclass of Granola::Serializer.
|
74
|
+
def initialize(list, serializer)
|
75
|
+
@item_serializer = serializer
|
76
|
+
@list = list.map { |obj| serializer.new(obj) }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public: Returns an Array of Hashes that can be serialized into JSON.
|
80
|
+
def attributes
|
81
|
+
@list.map(&:attributes)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: granola
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nicolas Sanguinetti
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: multi_json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: cutest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.2'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rack
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.5'
|
55
|
+
description: Granola is a very simple and fast library to turn your models to JSON
|
56
|
+
email:
|
57
|
+
- contacto@nicolassanguinetti.info
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- LICENSE
|
63
|
+
- README.md
|
64
|
+
- lib/granola.rb
|
65
|
+
- lib/granola/caching.rb
|
66
|
+
- lib/granola/helper.rb
|
67
|
+
- lib/granola/rack.rb
|
68
|
+
- lib/granola/version.rb
|
69
|
+
homepage: http://github.com/foca/granola
|
70
|
+
licenses:
|
71
|
+
- MIT
|
72
|
+
metadata: {}
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 2.2.2
|
90
|
+
signing_key:
|
91
|
+
specification_version: 4
|
92
|
+
summary: 'Granola: JSON Serializers for your app.'
|
93
|
+
test_files: []
|