├── .gitignore ├── init.rb ├── Gemfile ├── spec ├── fixtures │ ├── user.apibuilder │ └── users.apibuilder ├── api_builder │ └── renderer_spec.rb └── spec_helper.rb ├── lib ├── api_builder.rb └── api_builder │ ├── template.rb │ ├── with_name.rb │ └── renderer.rb ├── api_builder.gemspec ├── Rakefile ├── MIT-LICENSE ├── Gemfile.lock └── README.rdoc /.gitignore: -------------------------------------------------------------------------------- 1 | .rvmrc -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'api_builder' -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /spec/fixtures/user.apibuilder: -------------------------------------------------------------------------------- 1 | element :user do 2 | id user.id 3 | name user.name 4 | end -------------------------------------------------------------------------------- /lib/api_builder.rb: -------------------------------------------------------------------------------- 1 | require 'api_builder/with_name' 2 | require 'api_builder/renderer' 3 | require 'api_builder/template' -------------------------------------------------------------------------------- /spec/fixtures/users.apibuilder: -------------------------------------------------------------------------------- 1 | array :users do 2 | users.each do |user| 3 | element :user do 4 | id user.id 5 | name user.name 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /spec/api_builder/renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ApiBuilder::Renderer do 4 | 5 | it "can render" do 6 | user = OpenStruct.new(id: 1, name: "Api Builder") 7 | result = render("user", user: user) 8 | result.must_match '"id":1' 9 | result.must_match '"name":"Api Builder"' 10 | end 11 | 12 | end -------------------------------------------------------------------------------- /lib/api_builder/template.rb: -------------------------------------------------------------------------------- 1 | require 'action_view/base' 2 | require 'action_view/template' 3 | 4 | module ActionView 5 | module Template::Handlers 6 | class ApiBuilder 7 | 8 | def self.call(template) 9 | " 10 | extend ApiBuilder::Renderer 11 | #{template.source} 12 | get_output 13 | " 14 | end 15 | end 16 | end 17 | end 18 | 19 | ActionView::Template.register_template_handler :apibuilder, ActionView::Template::Handlers::ApiBuilder 20 | -------------------------------------------------------------------------------- /api_builder.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "api_builder" 3 | s.version = "1.0.2" 4 | 5 | s.author = "Lasse Bunk" 6 | s.email = "lassebunk@gmail.com" 7 | s.description = "ApiBuilder is a Ruby on Rails template engine that allows for multiple formats being laid out in a single specification, currently XML and JSON." 8 | s.summary = "Multiple API formats from a single specification." 9 | s.homepage = "http://github.com/lassebunk/api_builder" 10 | 11 | s.add_development_dependency("actionpack") 12 | 13 | s.files = Dir['lib/**/*.rb'] 14 | s.require_paths = ["lib"] 15 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require 'rake/testtask' 3 | require 'rdoc/task' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the api_builder plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.libs << 'spec' 12 | t.pattern = 'spec/**/*_spec.rb' 13 | t.verbose = true 14 | end 15 | 16 | desc 'Generate documentation for the api_builder plugin.' 17 | RDoc::Task.new(:rdoc) do |rdoc| 18 | rdoc.rdoc_dir = 'rdoc' 19 | rdoc.title = 'ApiBuilder' 20 | rdoc.options << '--line-numbers' << '--inline-source' 21 | rdoc.rdoc_files.include('README') 22 | rdoc.rdoc_files.include('lib/**/*.rb') 23 | end 24 | -------------------------------------------------------------------------------- /lib/api_builder/with_name.rb: -------------------------------------------------------------------------------- 1 | require 'builder' 2 | 3 | module ApiBuilder 4 | class HashWithName < Hash 5 | send :attr_accessor, :name 6 | 7 | def initialize(name) 8 | @name = name 9 | end 10 | 11 | def to_xml(options = {}) 12 | super options.update(:root => name, :skip_types => true, :dasherize => false) 13 | end 14 | end 15 | 16 | class ArrayWithName < Array 17 | send :attr_accessor, :name 18 | 19 | def initialize(name) 20 | @name = name 21 | end 22 | 23 | def to_xml(options = {}) 24 | super options.update(:root => name, :skip_types => true, :dasherize => false) 25 | end 26 | end 27 | 28 | class StringWithName < String 29 | send :attr_accessor, :name 30 | 31 | def initialize(name, value) 32 | @name = name 33 | super value.to_s 34 | end 35 | 36 | def to_xml(options = {}) 37 | if options[:builder] 38 | xml = options[:builder] 39 | else 40 | xml = Builder::XmlMarkup.new 41 | xml.instruct! 42 | end 43 | xml.tag! name, to_s 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Lasse Bunk 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | api_builder (1.0.2) 5 | 6 | GEM 7 | remote: http://rubygems.org/ 8 | specs: 9 | actionpack (3.1.1) 10 | activemodel (= 3.1.1) 11 | activesupport (= 3.1.1) 12 | builder (~> 3.0.0) 13 | erubis (~> 2.7.0) 14 | i18n (~> 0.6) 15 | rack (~> 1.3.2) 16 | rack-cache (~> 1.1) 17 | rack-mount (~> 0.8.2) 18 | rack-test (~> 0.6.1) 19 | sprockets (~> 2.0.2) 20 | activemodel (3.1.1) 21 | activesupport (= 3.1.1) 22 | builder (~> 3.0.0) 23 | i18n (~> 0.6) 24 | activesupport (3.1.1) 25 | multi_json (~> 1.0) 26 | builder (3.0.0) 27 | erubis (2.7.0) 28 | hike (1.2.1) 29 | i18n (0.6.0) 30 | multi_json (1.0.3) 31 | rack (1.3.5) 32 | rack-cache (1.1) 33 | rack (>= 0.4) 34 | rack-mount (0.8.3) 35 | rack (>= 1.0.0) 36 | rack-test (0.6.1) 37 | rack (>= 1.0) 38 | sprockets (2.0.3) 39 | hike (~> 1.2) 40 | rack (~> 1.0) 41 | tilt (~> 1.1, != 1.3.0) 42 | tilt (1.3.3) 43 | 44 | PLATFORMS 45 | ruby 46 | 47 | DEPENDENCIES 48 | actionpack 49 | activesupport 50 | api_builder! 51 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | require 'minitest/autorun' 3 | 4 | require 'json' 5 | require 'logger' 6 | require 'action_view' 7 | require 'api_builder' 8 | 9 | class MiniTest::Spec 10 | 11 | Handler = ActionView::Template::Handlers::ApiBuilder 12 | 13 | class LookupContext 14 | def disable_cache 15 | yield 16 | end 17 | 18 | def find_template(*args) 19 | end 20 | end 21 | 22 | class Context 23 | def initialize 24 | @output_buffer = "original" 25 | @virtual_path = nil 26 | end 27 | 28 | def params 29 | {} 30 | end 31 | 32 | def request 33 | OpenStruct.new(format: :json) 34 | end 35 | 36 | def partial 37 | ActionView::Template.new( 38 | "<%= @virtual_path %>", 39 | "partial", 40 | Handler, 41 | :virtual_path => "partial") 42 | end 43 | 44 | def lookup_context 45 | @lookup_context ||= LookupContext.new 46 | end 47 | 48 | def logger 49 | Logger.new(STDERR) 50 | end 51 | end 52 | 53 | def render(fixture, locals = {}) 54 | path = File.expand_path("../fixtures/#{fixture}.apibuilder", __FILE__) 55 | body = File.read(path) 56 | tmpl = ActionView::Template.new(body, "#{fixture} template", Handler, { virtual_path: fixture }) 57 | tmpl.locals = locals.keys 58 | tmpl.render(Context.new, locals) 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /lib/api_builder/renderer.rb: -------------------------------------------------------------------------------- 1 | module ApiBuilder 2 | module Renderer 3 | def id(*args, &block) 4 | method_missing(:id, *args, &block) 5 | end 6 | 7 | def method_missing(name, *args, &block) 8 | @_out = {} if @_out.nil? 9 | 10 | if block_given? 11 | out = @_out 12 | @_out = {} 13 | block.call 14 | out[name] = @_out 15 | @_out = out 16 | else 17 | @_out[name] = args[0] 18 | end 19 | end 20 | 21 | def array(name, value = nil, &block) 22 | if @_out.nil? 23 | @_out = ArrayWithName.new(name) 24 | block.call 25 | elsif @_out.is_a?(Array) 26 | out = @_out 27 | @_out = ArrayWithName.new(name) 28 | block.call 29 | out << @_out 30 | @_out = out 31 | else # out is a hash 32 | out = @_out 33 | @_out = [] 34 | block.call 35 | out[name] = @_out 36 | @_out = out 37 | end 38 | end 39 | 40 | def element(name, value = nil, &block) 41 | if block_given? 42 | if @_out.nil? 43 | @_out = HashWithName.new(name) 44 | block.call 45 | elsif @_out.is_a?(Array) 46 | out = @_out 47 | @_out = HashWithName.new(name) 48 | block.call 49 | out << @_out 50 | @_out = out 51 | else # out is a hash 52 | out = @_out 53 | @_out = {} 54 | block.call 55 | out[name] = @_out 56 | @_out = out 57 | end 58 | elsif name.is_a?(Hash) 59 | if @_out.nil? 60 | @_out = StringWithName.new(name.keys[0], name.values[0]) 61 | elsif @_out.is_a?(Array) 62 | @_out << StringWithName.new(name.keys[0], name.values[0]) 63 | else # out is a hash 64 | @_out[name.keys[0]] = name.values[0] 65 | end 66 | else 67 | if @_out.nil? 68 | @_out = StringWithName.new(name, value) 69 | elsif @_out.is_a?(Array) 70 | @_out << StringWithName.new(name, value) 71 | else # out is a hash 72 | @_out[name] = value 73 | end 74 | end 75 | end 76 | 77 | def get_output 78 | format = request.format.to_sym 79 | case format 80 | when :json 81 | if params[:callback] 82 | "#{params[:callback]}(#{@_out.to_json})" 83 | else 84 | @_out.to_json 85 | end 86 | when :xml 87 | @_out.to_xml 88 | else 89 | raise ArgumentError, "unknown format '#{format}'" 90 | end 91 | end 92 | end 93 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == About 2 | 3 | ApiBuilder is a Ruby on Rails template engine that allows for multiple formats being laid out in a single specification, currently XML and JSON. 4 | 5 | If you only need JSON, I recommend the Jbuilder gem: https://github.com/rails/jbuilder 6 | 7 | == Installation 8 | 9 | In your Gemfile: 10 | 11 | gem 'api_builder' 12 | 13 | And run bundle install. 14 | 15 | == Examples 16 | 17 | In app/views/api/users/index.apibuilder: 18 | 19 | array :users do 20 | @users.each do |user| 21 | element :user do 22 | id @user.id 23 | name @user.name 24 | end 25 | end 26 | end 27 | 28 | Returns: 29 | 30 | [ 31 | { 32 | "id": 1234, 33 | "name": "Peter Jackson" 34 | }, 35 | { 36 | "id": 1235, 37 | "name": "Marilyn Monroe" 38 | } 39 | ] 40 | 41 | And the equivalent XML. 42 | 43 | In app/views/api/users/show.apibuilder: 44 | 45 | element :user do 46 | id @user.id 47 | name @user.name 48 | address do 49 | street @user.street 50 | city @user.city 51 | end 52 | array :interests do 53 | @user.interests.each do |interest| 54 | element :interest => interest.name 55 | end 56 | end 57 | end 58 | 59 | Returns: 60 | 61 | { 62 | "id": 1234, 63 | "name": "Peter Jackson", 64 | "address": { 65 | "street": "123 High Way", 66 | "city": "Gotham City" 67 | }, 68 | "interests": [ 69 | "Movies", 70 | "Computers", 71 | "Internet" 72 | ] 73 | } 74 | 75 | And the equivalent XML. 76 | 77 | You can then call your API like this: 78 | 79 | http://example.com/api/users?format=json 80 | 81 | or 82 | 83 | http://example.com/api/users?format=xml 84 | 85 | and so on. 86 | 87 | == More examples 88 | 89 | Here's some more examples to get you started. 90 | 91 | === Root element 92 | 93 | element :test => "value" 94 | 95 | === Element with reserved name 96 | 97 | element :element => "value" 98 | 99 | === Model element 100 | 101 | element :article => Article.first 102 | 103 | === Model array 104 | 105 | element :articles => Article.all 106 | 107 | == Features 108 | 109 | === Multiple formats 110 | 111 | ApiBuilder supports both JSON and XML. 112 | 113 | === JSONP requests (callback parameter) 114 | 115 | ApiBuilder supports JSONP requests. Just call your URL with a callback parameter, e.g.: 116 | 117 | http://example.com/api/users?format=json&callback=myCallback 118 | 119 | == Contributors 120 | 121 | * Lasse Bunk (creator) 122 | * Dennis Reimann 123 | 124 | == Support 125 | 126 | Questions and suggestions are welcome at lassebunk@gmail.com. 127 | My blog is at http://lassebunk.dk. 128 | 129 | Copyright (c) 2011 Lasse Bunk, released under the MIT license 130 | --------------------------------------------------------------------------------