api-responder 1.0.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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in api-responder.gemspec
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'rake'
8
+ gem 'minitest'
9
+ gem 'minitest-ansi'
10
+ gem 'mocha', '>= 0.13', :require => false
11
+ end
12
+
13
+ group :test do
14
+ gem 'actionpack'
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jan Graichen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Responders::ApiResponder
2
+
3
+ `ApiResponder` simplifies version dependent rendering of API resources using a custom responder and a mixin for decorators or models (decorators are recommended).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'api-responder'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install api-responder
18
+
19
+ ## Usage
20
+
21
+ Add `Responders::ApiResponder` to your responder chain:
22
+
23
+ ```ruby
24
+ class AppResponder < Responder
25
+ include Responders::ApiResponder
26
+ end
27
+
28
+ class MyController < ApplicationController
29
+ self.responder = ApiResponder
30
+ end
31
+ ```
32
+
33
+ Or use it with [plataformatec/responders](https://github.com/plataformatec/responders):
34
+
35
+ ```ruby
36
+ class MyController < ApplicationController
37
+ responders Responders::ApiResponder
38
+ end
39
+ ```
40
+
41
+ This will add an API version parameter to the options hash for formatting methods like `as_json`. You can override the formatting methods or just include `ApiResponder::Formattable`:
42
+
43
+ ```ruby
44
+ class MyModel
45
+ include ApiResponder::Formattable
46
+
47
+ api_formats :xml
48
+
49
+ def as_api_v1
50
+ {
51
+ id: id,
52
+ first_name: name.split.first
53
+ last_name: name.split.last
54
+ }
55
+ end
56
+
57
+ def as_api_v2
58
+ as_api_v1.merge name: "#{first_name} #{last_name}"
59
+ end
60
+ end
61
+ ```
62
+
63
+ This will add a handler for XML. You can specify any format your want. `ApiResponder::Formattable` will override the `to_{format}` (e.g. `to_xml`) method to call `to_{format}` on the API specific hash. JSON is supported via `as_json` by default.
64
+
65
+ The only included method to detect API version is matching the URL path `/api/v(\d+)`. Or you can add an `api_version` method to your controller:
66
+
67
+ ```ruby
68
+ class MyController < ApplicationController
69
+ responders Responders::ApiResponder
70
+
71
+ def api_version
72
+ return $1 if request.headers["Accept"] =~ /vnd\.myapp.v(\d+)/
73
+ end
74
+ end
75
+ ```
76
+
77
+ I recommend using `ApiResponder` in combination with [jgraichen/decorate-responder](/jgraichen/decorate-responder) and the decorator pattern (like [draper](/drapergem/draper)):
78
+
79
+ ```ruby
80
+ class User < ActiveRecord::Base
81
+ attr_accessible :id, :first_name, :last_name
82
+ end
83
+
84
+ class UserDecorator < Draper::Decorator
85
+ include ApiResponder::Formattable
86
+ decorates :user
87
+
88
+ api_formats :msgpack
89
+
90
+ def name
91
+ "#{first_name} #{last_name}"
92
+ end
93
+
94
+ def as_api_v1
95
+ {
96
+ id: model.id,
97
+ created_at: model.created_at.utc.iso8601,
98
+ name: name
99
+ }
100
+ end
101
+ end
102
+
103
+ class UsersController < ApplicationController
104
+ responders Responders::ApiResponder,
105
+ Responders::DecorateResponder
106
+
107
+ respond_to :json, :xml, :msgpack
108
+ rescue_from ApiResponder::Formattable::UnsupportedVersion do
109
+ head :not_acceptable
110
+ end
111
+
112
+ def index
113
+ respond_with User.scoped
114
+ end
115
+
116
+ def api_version
117
+ return $1 if request.headers["Accept"] =~ /vnd\.myapp.v(\d+)/
118
+ end
119
+ end
120
+ ```
121
+
122
+ When `ApiResponder::Formattable` receives nil as API version or the resource does not have a matching `as_api_v` method an `UnsupportedVersion` error will be raised. You can catch that exception and for example return a `406` status code:
123
+
124
+ ```ruby
125
+ rescue_from ApiResponder::Formattable::UnsupportedVersion do
126
+ head :not_acceptable
127
+ end
128
+ ```
129
+
130
+ Check out [jgraichen/paginate-responder](/jgraichen/paginate-responder) for automagic pagination support including HTTP Link headers.
131
+
132
+ ## Contributing
133
+
134
+ 1. Fork it
135
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
136
+ 3. Write tests for your feature
137
+ 4. Add your feature
138
+ 5. Commit your changes (`git commit -am 'Add some feature'`)
139
+ 6. Push to the branch (`git push origin my-new-feature`)
140
+ 7. Create new Pull Request
141
+
142
+ ## License
143
+
144
+ [MIT License](http://www.opensource.org/licenses/mit-license.php)
145
+
146
+ Copyright (c) 2013 Jan Graichen
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push "test"
6
+ t.test_files = FileList['test/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'api-responder'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "api-responder"
8
+ gem.version = ApiResponder::VERSION
9
+ gem.authors = ["Jan Graichen"]
10
+ gem.email = ["jg@altimos.de"]
11
+ gem.description = %q{ApiResponder simplifies version dependent rendering of API resources.}
12
+ gem.summary = %q{ApiResponder simplifies version dependent rendering of API resources.}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'activesupport'
21
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module ApiResponder
4
+ autoload :VERSION, 'api-responder/version'
5
+ autoload :Formattable, 'api-responder/formattable'
6
+ end
7
+
8
+ module Responders
9
+ autoload :ApiResponder, 'responders/api_responder'
10
+ end
@@ -0,0 +1,44 @@
1
+ module ApiResponder
2
+ module Formattable
3
+ class UnsupportedVersion < StandardError; end
4
+
5
+ module ClassMethods
6
+ def api_formats(*formats)
7
+ @api_formats ||= [ :json ]
8
+ return @api_formats if formats.empty?
9
+
10
+ formats.map!(&:to_sym)
11
+ formats -= @api_formats
12
+
13
+ formats.each do |format|
14
+ method = :"to_#{format}"
15
+ send :define_method, method do |options|
16
+ as_api(options.merge(:format => format)).send method, options
17
+ end
18
+ @api_formats << format
19
+ end
20
+ end
21
+ end
22
+
23
+ module InstanceMethods
24
+ def as_json(options)
25
+ as_api(options.merge(:format => :json)).as_json(options)
26
+ end
27
+
28
+ def as_api(options)
29
+ raise UnsupportedVersion.new unless options[:api_version]
30
+
31
+ method = :"as_api_v#{options[:api_version]}"
32
+ raise UnsupportedVersion.new unless respond_to? method
33
+
34
+ options.delete(:api_version)
35
+ return send method, options
36
+ end
37
+ end
38
+
39
+ def self.included(receiver)
40
+ receiver.extend ClassMethods
41
+ receiver.send :include, InstanceMethods
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ module ApiResponder
2
+ module VERSION
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ PATCH = 0
6
+ STAGE = nil
7
+
8
+ def self.to_s
9
+ [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.')
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ module Responders
2
+ module ApiResponder
3
+ def options
4
+ super.merge(api_options)
5
+ end
6
+
7
+ def api_options
8
+ { :api_version => api_version }
9
+ end
10
+
11
+ def api_version
12
+ return controller.api_version if controller.respond_to? :api_version
13
+ detect_api_version
14
+ end
15
+
16
+ def detect_api_version
17
+ return $1.to_i if request.path =~ /^\/api\/v(\d+)\//
18
+ nil
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ require 'test_helper'
2
+
3
+ class ApiResponderTest < ActionController::TestCase
4
+ tests AppController
5
+
6
+ def setup
7
+ @resource = Resource.new
8
+ end
9
+
10
+ def test_formats_resource
11
+ get :index, :format => :json, :resource => Resource.new
12
+
13
+ assert_equal 406, response.status
14
+ end
15
+
16
+ def test_version_from_path
17
+ get :v2, :format => :json, :resource => Resource.new
18
+
19
+ assert_equal({"av" => 2}, JSON[response.body])
20
+ end
21
+
22
+ def test_custom_version
23
+ @controller = CustomController.new
24
+ @resource.expects(:as_api_v4).returns({"av" => "c4"})
25
+
26
+ request.env["HTTP_ACCEPT"] = "application/vnd.test.v4+json"
27
+ get :index, {:format => :json, :resource => @resource}
28
+
29
+ assert_equal({"av" => "c4"}, JSON[response.body])
30
+ end
31
+
32
+ def test_custom_version_nil
33
+ @controller = CustomController.new
34
+
35
+ request.env["HTTP_ACCEPT"] = "application/vnd.test.v+json"
36
+ get :index, {:format => :json, :resource => @resource}
37
+
38
+ assert_equal 406, response.status
39
+ end
40
+
41
+ def test_custom_version_unsupported
42
+ @controller = CustomController.new
43
+
44
+ request.env["HTTP_ACCEPT"] = "application/vnd.test.v6+json"
45
+ get :index, {:format => :json, :resource => @resource}
46
+
47
+ assert_equal 406, response.status
48
+ end
49
+ end
@@ -0,0 +1,63 @@
1
+ require 'test_helper'
2
+
3
+ describe ApiResponder::Formattable do
4
+ let(:resource) { Object.new }
5
+ let(:cls) { Class.new.tap { |c| c.send :include, ApiResponder::Formattable }}
6
+ let(:decorator) { cls.new }
7
+
8
+ describe "#api_formats" do
9
+ it "should have active json as default" do
10
+ assert_equal [ :json ], cls.api_formats
11
+ end
12
+
13
+ it "should return list of active formats" do
14
+ cls.api_formats :xml
15
+ assert_equal [ :json, :xml ], cls.api_formats
16
+ end
17
+
18
+ it "should have as_json method that delegates to as_api" do
19
+ decorator.expects(:as_api).with({:format => :json}).returns(resource)
20
+ resource.expects(:as_json).with({}).returns({})
21
+ decorator.as_json({})
22
+ end
23
+
24
+ it "should create to_format method that delegates to as_api" do
25
+ cls.api_formats :xml
26
+ decorator.expects(:as_api).with({:format => :xml}).returns(resource)
27
+ resource.expects(:to_xml).with({}).returns("")
28
+
29
+ decorator.to_xml({})
30
+ end
31
+ end
32
+
33
+ describe "#to_format" do
34
+ it "should merge options" do
35
+ cls.api_formats :xml
36
+ decorator.expects(:as_api).with({:format => :xml, :include_root => true}).returns(resource)
37
+ resource.expects(:to_xml).with({:include_root => true}).returns("")
38
+
39
+ decorator.to_xml({:include_root => true})
40
+ end
41
+ end
42
+
43
+ describe "#as_api" do
44
+ it "should delegate to to_api_v{api_version}" do
45
+ decorator.expects(:as_api_v2).with({:format => :json}).returns({})
46
+
47
+ decorator.as_api({:format => :json, :api_version => 2})
48
+ end
49
+
50
+ it "should raise exception if no version is given" do
51
+ assert_raises ApiResponder::Formattable::UnsupportedVersion do
52
+ decorator.as_api({:format => :json})
53
+ end
54
+ end
55
+
56
+ it "should raise exception if no version helper exists" do
57
+ assert_raises ApiResponder::Formattable::UnsupportedVersion do
58
+ decorator.as_api({:format => :json, :api_version => 4})
59
+ end
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,57 @@
1
+ require 'minitest/autorun'
2
+ require 'bundler'
3
+
4
+ Bundler.setup
5
+
6
+ # Configure Rails
7
+ ENV["RAILS_ENV"] = "test"
8
+
9
+ require 'mocha'
10
+ require 'minitest/ansi'
11
+ MiniTest::ANSI.use!
12
+
13
+ require 'active_support'
14
+ require 'action_controller'
15
+
16
+
17
+ Responders::Routes = ActionDispatch::Routing::RouteSet.new
18
+ Responders::Routes.draw do
19
+ match '/' => 'app#index'
20
+ match '/api/v1/index' => 'app#v1'
21
+ match '/api/v2/index' => 'app#v2'
22
+ match '/index' => 'custom#index'
23
+ end
24
+
25
+ class ActiveSupport::TestCase
26
+ setup do @routes = Responders::Routes end
27
+ end
28
+
29
+ class AppResponder < ActionController::Responder
30
+ include Responders::ApiResponder
31
+ end
32
+
33
+ class AppController < ActionController::Base
34
+ include Responders::Routes.url_helpers
35
+ self.responder = AppResponder
36
+ respond_to :json
37
+ rescue_from ApiResponder::Formattable::UnsupportedVersion do head :not_acceptable end
38
+ def index; respond_with params[:resource]; end
39
+ def v1; respond_with params[:resource]; end
40
+ def v2; respond_with params[:resource]; end
41
+ end
42
+
43
+ class CustomController < ActionController::Base
44
+ include Responders::Routes.url_helpers
45
+ self.responder = AppResponder
46
+ respond_to :json
47
+ rescue_from ApiResponder::Formattable::UnsupportedVersion do head :not_acceptable end
48
+ def index; respond_with params[:resource]; end
49
+ def api_version; return $1 if request.headers["Accept"] =~ /vnd\.test.v(\d+)/; end
50
+ end
51
+
52
+ class Resource
53
+ include ApiResponder::Formattable
54
+
55
+ def as_api_v1(opts); { :av => 1 }; end
56
+ def as_api_v2(opts); { :av => 2 }; end
57
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api-responder
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jan Graichen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: ApiResponder simplifies version dependent rendering of API resources.
31
+ email:
32
+ - jg@altimos.de
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - ".gitignore"
38
+ - Gemfile
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - api-responder.gemspec
43
+ - lib/api-responder.rb
44
+ - lib/api-responder/formattable.rb
45
+ - lib/api-responder/version.rb
46
+ - lib/responders/api_responder.rb
47
+ - test/api_responder_test.rb
48
+ - test/formattable_test.rb
49
+ - test/test_helper.rb
50
+ homepage: ''
51
+ licenses: []
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ segments:
63
+ - 0
64
+ hash: -1741048395832599252
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ segments:
72
+ - 0
73
+ hash: -1741048395832599252
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 1.8.25
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: ApiResponder simplifies version dependent rendering of API resources.
80
+ test_files:
81
+ - test/api_responder_test.rb
82
+ - test/formattable_test.rb
83
+ - test/test_helper.rb