├── .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 |
--------------------------------------------------------------------------------