granola 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Granola
2
+ VERSION = "0.0.1"
3
+ 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: []