├── 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 | [![Build Status](https://secure.travis-ci.org/tatey/porth.png)](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 | --------------------------------------------------------------------------------