├── test
├── fixtures
│ ├── hash.rb
│ ├── array.rb
│ ├── instance_variable.rb
│ └── block.rb
├── porth
│ ├── handler_test.rb
│ ├── rendering
│ │ ├── json_test.rb
│ │ └── xml_test.rb
│ └── format
│ │ └── xml_test.rb
└── test_helper.rb
├── .gitignore
├── lib
├── porth
│ ├── version.rb
│ ├── unknown_format_error.rb
│ ├── format
│ │ ├── json.rb
│ │ └── xml.rb
│ └── handler.rb
└── porth.rb
├── .travis.yml
├── Gemfile
├── CHANGELOG.md
├── Rakefile
├── porth.gemspec
├── LICENSE
└── README.md
/test/fixtures/hash.rb:
--------------------------------------------------------------------------------
1 | {:foo => 'bar'}
2 |
--------------------------------------------------------------------------------
/test/fixtures/array.rb:
--------------------------------------------------------------------------------
1 | [{:foo => 'bar'}]
2 |
--------------------------------------------------------------------------------
/test/fixtures/instance_variable.rb:
--------------------------------------------------------------------------------
1 | [@foo]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 |
--------------------------------------------------------------------------------
/lib/porth/version.rb:
--------------------------------------------------------------------------------
1 | module Porth
2 | VERSION = '0.0.2'
3 | end
4 |
--------------------------------------------------------------------------------
/test/fixtures/block.rb:
--------------------------------------------------------------------------------
1 | (1..2).map do |n|
2 | {:value => n}
3 | end
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 1.8.7
4 | - 1.9.2
5 | - 1.9.3
6 | - jruby
7 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in porbt.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/lib/porth/unknown_format_error.rb:
--------------------------------------------------------------------------------
1 | module Porth
2 | class UnknownFormatError < StandardError
3 | def message
4 | 'Unknown format. Supported formats are ' + Handler.formats.keys.join(', ')
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## 0.0.2 / 2012-01-21
4 |
5 | * XML singularizes resource name when view returns a single object
6 | * Relaxed dependancy versions. Works with Rails 3.0.0+. No longer explicitly depends on JSON.
7 |
8 | ## 0.0.1 / 2011-10-23
9 |
10 | * Initial release
11 |
--------------------------------------------------------------------------------
/test/porth/handler_test.rb:
--------------------------------------------------------------------------------
1 | require 'action_view'
2 | require 'test_helper'
3 |
4 | class HandlerTest < MiniTest::Unit::TestCase
5 | def test_unsupported_format
6 | assert_raises UnknownFormatError do
7 | Handler.new(ActionView::Template.new(nil, nil, nil, {:format => 'text/html'})).format
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/porth.rb:
--------------------------------------------------------------------------------
1 | require 'action_controller'
2 |
3 | require 'porth/format/json'
4 | require 'porth/format/xml'
5 | require 'porth/handler'
6 | require 'porth/unknown_format_error'
7 |
8 | Porth::Handler.register_format :json, Porth::Format::JSON
9 | Porth::Handler.register_format :xml, Porth::Format::XML
10 |
11 | ActionView::Template.register_template_handler :rb, Porth::Handler
12 |
--------------------------------------------------------------------------------
/lib/porth/format/json.rb:
--------------------------------------------------------------------------------
1 | module Porth
2 | module Format
3 | module JSON
4 | def self.call controller, object
5 | json = object.to_json
6 | callback = controller.send :json_callback
7 | callback ? "#{callback}(#{json})" : json
8 | end
9 |
10 | module ControllerExtensions
11 | protected
12 |
13 | def json_callback
14 | params[:callback]
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require 'bundler'
3 | rescue LoadError
4 | raise "Could not load the bundler gem. Install it with `gem install bundler`."
5 | end
6 |
7 | begin
8 | Bundler.setup
9 | rescue Bundler::GemNotFound
10 | raise RuntimeError, "Bundler couldn't find some gems. Did you run `bundle install`?"
11 | end
12 |
13 | require 'bundler/gem_tasks'
14 | require 'rake/testtask'
15 |
16 | Rake::TestTask.new :test do |test|
17 | test.libs << 'test'
18 | test.pattern = 'test/**/*_test.rb'
19 | end
20 |
21 | task :default => :test
22 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'porth'
3 |
4 | module Rendering
5 | class MockController < ActionController::Base
6 | def params
7 | @params ||= {}
8 | end
9 | end
10 |
11 | def render file, format, controller, assigns = {}
12 | ActionView::Base.new(template_dir, assigns, controller).tap do |view|
13 | view.lookup_context.freeze_formats [format]
14 | end.render :file => file
15 | end
16 |
17 | def template_dir
18 | File.expand_path File.dirname(__FILE__) + '/fixtures'
19 | end
20 | end
21 |
22 | include Porth
23 |
--------------------------------------------------------------------------------
/test/porth/rendering/json_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Rendering::JSONTest < MiniTest::Unit::TestCase
4 | include Rendering
5 |
6 | def test_render_json
7 | assert_equal '[{"value":1},{"value":2}]', render('block', :json, MockController.new)
8 | end
9 |
10 | def test_render_json_with_callback
11 | controller = MockController.new
12 | controller.params[:callback] = 'myFunction'
13 | assert_equal 'myFunction([{"value":1},{"value":2}])', render('block', :json, controller)
14 | end
15 |
16 | def test_render_json_with_instance_variable
17 | assert_equal '["bar"]', render('instance_variable', :json, MockController.new, {'foo' => 'bar'})
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/porth/handler.rb:
--------------------------------------------------------------------------------
1 | module Porth
2 | class Handler
3 | attr_reader :template
4 |
5 | def initialize template
6 | @template = template
7 | end
8 |
9 | def call
10 | "#{format}.call controller, instance_eval { #{template.source} }"
11 | end
12 |
13 | def format
14 | self.class.formats.fetch(template.formats.first) { raise UnknownFormatError }
15 | end
16 |
17 | def self.call template
18 | new(template).call
19 | end
20 |
21 | def self.formats
22 | @formats ||= {}
23 | end
24 |
25 | def self.register_format format, mod
26 | formats[format] = mod
27 | ActionController::Base.send :include, mod::ControllerExtensions
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/porth/format/xml.rb:
--------------------------------------------------------------------------------
1 | module Porth
2 | module Format
3 | module XML
4 | def self.call controller, object
5 | method = if object.respond_to?(:count) && !object.respond_to?(:keys)
6 | :xml_pluralized_root
7 | else
8 | :xml_singularized_root
9 | end
10 | object.to_xml :root => controller.send(method)
11 | end
12 |
13 | module ControllerExtensions
14 | protected
15 |
16 | def xml_root
17 | self.class.name.sub('Controller', '').underscore.split('/').last
18 | end
19 |
20 | def xml_pluralized_root
21 | xml_root.pluralize
22 | end
23 |
24 | def xml_singularized_root
25 | xml_root.singularize
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/porth.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | $:.push File.expand_path("../lib", __FILE__)
4 | require "porth/version"
5 |
6 | Gem::Specification.new do |s|
7 | s.name = "porth"
8 | s.version = Porth::VERSION
9 | s.platform = Gem::Platform::RUBY
10 | s.authors = ["Tate Johnson"]
11 | s.email = ["tate@tatey.com"]
12 | s.homepage = "https://github.com/tatey/porth"
13 | s.summary = %q{Plain Old Ruby Template Handler}
14 | s.description = %q{Write your views using plain old Ruby}
15 |
16 | s.rubyforge_project = "porbt"
17 |
18 | s.files = `git ls-files`.split "\n"
19 | s.test_files = `git ls-files -- {test,spec,features}/*`.split "\n"
20 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21 | s.require_paths = ["lib"]
22 |
23 | s.add_runtime_dependency 'actionpack', '>= 3.0.0', '< 4.0.0'
24 |
25 | s.add_development_dependency 'minitest', '~> 2.6.2'
26 | s.add_development_dependency 'rake', '~> 0.9.2'
27 | end
28 |
--------------------------------------------------------------------------------
/test/porth/rendering/xml_test.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/hash'
2 | require 'rexml/document'
3 | require 'test_helper'
4 |
5 | class Rendering::XMLTest < MiniTest::Unit::TestCase
6 | include Rendering
7 |
8 | def test_render_xml
9 | assert_equal "\n\n \n 1\n \n \n 2\n \n\n", render(:block, :xml, MockController.new)
10 | end
11 |
12 | def test_render_xml_with_array_of_objects
13 | xml = REXML::Document.new render('array', :xml, MockController.new)
14 | assert_equal 'mocks', xml.root.name
15 | end
16 |
17 | def test_render_xml_with_object
18 | xml = REXML::Document.new render('hash', :xml, MockController.new)
19 | assert_equal 'mock', xml.root.name
20 | end
21 |
22 | def test_render_xml_with_instance_variable
23 | assert_equal "\n\n bar\n\n", render('instance_variable', :xml, MockController.new, {'foo' => 'bar'})
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2011 Tate Johnson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/porth/format/xml_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Format::XMLTest < MiniTest::Unit::TestCase
4 | def mock_controller name
5 | instance_eval <<-RUBY_STRING
6 | Class.new do
7 | include Format::XML::ControllerExtensions
8 |
9 | public :xml_root, :xml_pluralized_root, :xml_singularized_root
10 |
11 | def self.name
12 | '#{name}'
13 | end
14 | end
15 | RUBY_STRING
16 | end
17 |
18 | def test_xml_root
19 | assert_equal 'foo', mock_controller('Foo').new.xml_root
20 | assert_equal 'foo', mock_controller('FooController').new.xml_root
21 | assert_equal 'foos', mock_controller('FoosController').new.xml_root
22 | assert_equal 'bar', mock_controller('Foo::BarController').new.xml_root
23 | end
24 |
25 | def test_xml_pluralized_root
26 | assert_equal 'foos', mock_controller('Foo').new.xml_pluralized_root
27 | assert_equal 'foos', mock_controller('Foos').new.xml_pluralized_root
28 | end
29 |
30 | def test_xml_singularized_root
31 | assert_equal 'foo', mock_controller('Foo').new.xml_singularized_root
32 | assert_equal 'foo', mock_controller('Foos').new.xml_singularized_root
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deprecated
2 |
3 | **TL;DR** This library is deprecated and will no longer be maintained. You should consider
4 | migrating to [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
5 |
6 | Porth was a joy to create and use, but it's time to recognise when other libraries
7 | do it better. The principle idea behind Porth was to separate the JSON and XML
8 | representations of your models.
9 |
10 | [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers)
11 | is a better library with a tonne of support from the community. I would personally
12 | choose this for my next API.
13 |
14 | Thank you for supporting Porth.
15 |
16 | # Porth (Plain Old Ruby Template Handler)
17 |
18 | [](http://travis-ci.org/tatey/porth)
19 |
20 | Write your views using plain old Ruby. Views are for representation, not defining
21 | `#as_json` in a model. There's no need to learn a DSL for building arrays and hashes.
22 | Just use Ruby. Views are written once and rendered in JSON(P) or XML based on
23 | the requested format. Porth makes few assumptions and can be configured.
24 |
25 | ## Installation
26 |
27 | Add this to your project's Gemfile and run `$ bundle install`
28 |
29 | ``` ruby
30 | gem 'porth'
31 | ```
32 |
33 | ## Usage
34 |
35 | Create a controller that responds to JSON, XML or both.
36 |
37 | ``` ruby
38 | # app/controllers/transmitters_controller.rb
39 | class TransmittersController < ApplicationController
40 | respond_to :json, :xml
41 |
42 | def index
43 | @transmitters = Transmitter.all
44 | respond_with @transmitters
45 | end
46 |
47 | # ...
48 | end
49 | ````
50 |
51 | Create a template with a `.rb` extension. Write plain old Ruby. Objects
52 | must respond to `#to_json` or `#to_xml`. Arrays and hashes are best.
53 |
54 | ``` ruby
55 | # app/views/transmitters/index.rb
56 | @transmitters.map do |t|
57 | {
58 | name: t.area_served,
59 | latitude: t.latitude,
60 | longitude: t.longitude,
61 | nearby: t.nearby.size
62 | }
63 | end
64 | ```
65 |
66 | GET /transmitters.json
67 |
68 | ``` javascript
69 | [{"name":"Brisbane","latitude":-27.4661111111111,"longitude":152.946388888889,"nearby":11}]
70 | ```
71 |
72 | GET /transmitters.json?callback=myFunction
73 |
74 | ``` javascript
75 | myFunction([{"name":"Brisbane","latitude":-27.4661111111111,"longitude":152.946388888889,"nearby":11}])
76 | ```
77 |
78 | GET /transmitters.xml
79 |
80 | ``` xml
81 |
82 |
83 |
84 | Brisbane
85 | -27.4661111111111
86 | 152.946388888889
87 | 11
88 |
89 |
90 | ```
91 |
92 | `Porth::UnknownFormatError` is raised when the requested format is not supported.
93 |
94 | ### JSONP
95 |
96 | Porth calls `#json_callback` to get the function name for JSONP responses. By default
97 | this method is added to `ActionController::Base` and returns `params[:callback]`. Override
98 | `#json_callback` to get different behaviour.
99 |
100 | ``` ruby
101 | class ApplicationController < ActionController::Base
102 | # ...
103 |
104 | protected
105 |
106 | def json_callback
107 | nil # Ignore JSONP requests
108 | end
109 | end
110 | ```
111 |
112 | ### XML
113 |
114 | Porth guesses the resource's name from the controller's class. `Foo::BarsController`
115 | becomes `bars`. Override `#xml_root` to explicitly set the resource name.
116 |
117 | ``` ruby
118 | class TransmittersController < ApplicationController
119 | # ...
120 |
121 | protected
122 |
123 | def xml_root
124 | 'sites'
125 | end
126 | end
127 | ```
128 |
129 | Resource names are pluralized or singularized by introspecting the return type from
130 | the view. Following convention, collection actions (index) should return
131 | an array of objects and member actions (new, create, edit, update, delete) should
132 | return an object. Override `#xml_pluralized_root` to explicitly set the collection
133 | resource name and override `#xml_singularize_root` to explicitly set the member
134 | resource name.
135 |
136 | ``` ruby
137 | class SeaFoodsController < ApplicationController
138 | # ...
139 |
140 | protected
141 |
142 | def xml_pluralized_root
143 | 'fish'
144 | end
145 |
146 | def xml_singularized_root
147 | 'fish'
148 | end
149 | end
150 | ```
151 |
152 | ## Examples
153 |
154 | Remember, anything you can do in Ruby you can do in Porth. Here are a few ideas
155 | for writing and testing your views.
156 |
157 | ### Subset
158 |
159 | Conveniently select a subset of attributes.
160 |
161 | ``` ruby
162 | # app/views/users/show.rb
163 | @author.attributes.slice 'id', 'first_name', 'last_name', 'email'
164 | ```
165 |
166 | ### Variable and Condition
167 |
168 | Hashes may get dirty if you attempt to build them all in one go. Consider storing
169 | the hash in a variable and adding to it based on a condition. Like a method you
170 | need to return the hash on the last line.
171 |
172 | ``` ruby
173 | # app/views/users/show.rb
174 | attributes = @author.attributes.slice 'id', 'first_name', 'last_name', 'email'
175 | if current_user.admin?
176 | attributes['ip_address'] = @author.ip_address
177 | attributes['likability'] = @author.determine_likability_as_of Time.current
178 | end
179 | attributes
180 | ```
181 |
182 | ### Functional Test
183 |
184 | Use functional tests to verify the response's body is correct.
185 |
186 | ``` ruby
187 | # app/views/posts/show.rb
188 | @author.attributes.slice 'id', 'title', 'body'
189 | ```
190 |
191 | JSON maps well to Ruby's hashes. Set the response to JSON, parse the body into
192 | a hash and verify the key-value pairs.
193 |
194 | ``` ruby
195 | # test/functional/posts_controller_test.rb
196 | require 'test_helper'
197 |
198 | class PostsControllerTest < ActionController::TestCase
199 | # ...
200 |
201 | test "GET show" do
202 | get :show, id: posts(:hello_word).id, format: 'json'
203 | post = JSON.parse response.body
204 | assert_equal 123040040, post['id']
205 | assert_equal 'Hello, World!', post['title']
206 | assert_equal 'Lorem ipsum dolar sit amet...', post['body']
207 | end
208 | end
209 | ```
210 |
211 | ## Compatibility
212 |
213 | * MRI 1.8.7
214 | * MRI 1.9.2+
215 | * JRuby 1.6.4+
216 |
217 | ## Dependancies
218 |
219 | * ActionPack 3.0.0+
220 |
221 | ## Extensions
222 |
223 | * [porth-plist](https://github.com/soundevolution/porth-plist) - Adds Property list (.plist) support
224 |
225 | ## Contributing
226 |
227 | 1. Fork
228 | 2. Install dependancies by running `$ bundle install`
229 | 3. Write tests and code
230 | 4. Make sure the tests pass by running `$ bundle exec rake`
231 | 5. Push and send a pull request on GitHub
232 |
233 | ## Credits
234 |
235 | Porth is the result of numerous discussions at [Everyday Hero](http://www.everydayhero.com.au)
236 | around better API design.
237 |
238 | ## Copyright
239 |
240 | Copyright © 2011 Tate Johnson. Released under the MIT license. See LICENSE.
241 |
--------------------------------------------------------------------------------