├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── json_builder.gemspec ├── lib ├── json_builder.rb └── json_builder │ ├── compiler.rb │ ├── elements.rb │ ├── extensions.rb │ ├── member.rb │ ├── template.rb │ ├── value.rb │ └── version.rb └── test ├── benchmarks └── builder.rb ├── compiler_test.rb ├── elements_test.rb ├── extensions_test.rb ├── member_test.rb ├── test_helper.rb └── value_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | doc/* 3 | .DS_Store 4 | *.log 5 | /.rbenv-version 6 | 7 | # Bundle settings 8 | /.bundle 9 | 10 | # Coverage report 11 | /coverage 12 | 13 | # Gem install location 14 | /vendor 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.2 5 | - 1.9.3 6 | - jruby 7 | - jruby-18mode 8 | - jruby-19mode 9 | - jruby-head 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rake' 6 | gem 'coveralls', :require => false 7 | 8 | group :test do 9 | gem 'i18n' 10 | gem 'tzinfo' 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | json_builder (3.1.7) 5 | activesupport (>= 2.0.0) 6 | json 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | activesupport (3.2.8) 12 | i18n (~> 0.6) 13 | multi_json (~> 1.0) 14 | colorize (0.5.8) 15 | coveralls (0.6.0) 16 | colorize 17 | multi_json (~> 1.3) 18 | rest-client 19 | simplecov (>= 0.7) 20 | thor 21 | i18n (0.6.0) 22 | json (1.7.5) 23 | mime-types (1.21) 24 | multi_json (1.3.6) 25 | rake (0.9.2.2) 26 | rest-client (1.6.7) 27 | mime-types (>= 1.16) 28 | simplecov (0.7.1) 29 | multi_json (~> 1.0) 30 | simplecov-html (~> 0.7.1) 31 | simplecov-html (0.7.1) 32 | thor (0.17.0) 33 | tzinfo (0.3.31) 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | coveralls 40 | i18n 41 | json_builder! 42 | rake 43 | tzinfo 44 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Garrett Bjerkhoel 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *No longer maintained, please check out [jbuilder](https://github.com/rails/jbuilder)* 2 | 3 | JSON Builder [![Build Status](https://secure.travis-ci.org/dewski/json_builder.png)](http://travis-ci.org/dewski/json_builder) 4 | ============ 5 | Rails provides an excellent XML Builder by default to build RSS and ATOM feeds, but nothing to help you build complex and custom JSON data structures. The standard `to_json` works just fine, but can get very verbose when you need full control of what is generated and performance is a factor. JSON Builder hopes to solve that problem. 6 | 7 | ## Sample Usage 8 | 9 | ```ruby 10 | require 'json_builder' 11 | 12 | json = JSONBuilder::Compiler.generate do 13 | name 'Garrett Bjerkhoel' 14 | email 'spam@garrettbjerkhoel.com' 15 | url user_url(user) 16 | address do 17 | street '1234 1st Ave' 18 | city 'New York' 19 | state 'NY' 20 | zip 10065 21 | end 22 | key :nil, 'testing a custom key name' 23 | skills do 24 | ruby true 25 | asp false 26 | end 27 | longstring do 28 | # Could be a highly intensive process that only returns a string 29 | '12345' * 25 30 | end 31 | end 32 | ``` 33 | 34 | Which will generate: 35 | 36 | ```json 37 | { 38 | "name": "Garrett Bjerkhoel", 39 | "email": "spam@garrettbjerkhoel.com", 40 | "url": "http://examplesite.com/dewski", 41 | "address": { 42 | "street": "1234 1st Ave", 43 | "city": "New York", 44 | "state": "NY", 45 | "zip": 10065 46 | }, 47 | "nil": "testing a custom key name", 48 | "skills": { 49 | "ruby": true, 50 | "asp": false 51 | }, 52 | "longstring": "1234512345123451234512345..." 53 | } 54 | ``` 55 | 56 | If you'd like to just generate an array: 57 | 58 | ```ruby 59 | array ['Garrett Bjerkhoel', 'John Doe'] do |name| 60 | first, last = name.split(' ') 61 | first first 62 | last last 63 | end 64 | ``` 65 | 66 | Which will output the following: 67 | 68 | ```json 69 | [ 70 | { 71 | "first": "Garrett", 72 | "last": "Bjerkhoel" 73 | }, 74 | { 75 | "first": "John", 76 | "last": "Doe" 77 | } 78 | ] 79 | ``` 80 | 81 | Just a note, if you use an array block, all other builder methods will be ignored. 82 | 83 | ## Using JSON Builder with Rails 84 | First, make sure to add the gem to your `Gemfile`. 85 | 86 | ```ruby 87 | gem 'json_builder' 88 | ``` 89 | 90 | Second, make sure your controller responds to `json`: 91 | 92 | ```ruby 93 | class UsersController < ApplicationController 94 | respond_to :json 95 | 96 | def index 97 | @users = User.order('id DESC').page(params[:page]).per(2) 98 | respond_with @users 99 | end 100 | end 101 | ``` 102 | 103 | Lastly, create `app/views/users/index.json.json_builder` which could look something like: 104 | 105 | ```ruby 106 | count @users.count 107 | page @users.current_page 108 | per_page @users.per_page 109 | pages_count @users.num_pages 110 | results @users do |user| 111 | id user.id 112 | name user.name 113 | body user.body 114 | url user_url(user) 115 | links user.links do |link| 116 | url link.url 117 | visits link.visits 118 | last_visited link.last_visited 119 | end 120 | end 121 | ``` 122 | 123 | You will get something like: 124 | 125 | ```json 126 | { 127 | "count": 10, 128 | "page": 1, 129 | "per_page": 2, 130 | "pages_count": 5, 131 | "results": [ 132 | { 133 | "id": 1, 134 | "name": "Garrett Bjerkhoel", 135 | "body": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod.", 136 | "url": "http://example.com/users/garrett-bjerkhoel", 137 | "links": [ 138 | { 139 | "url": "http://github.com/", 140 | "visits": 500, 141 | "last_visited": "2011-11-271T00:00:01Z" 142 | }, 143 | { 144 | "url": "http://garrettbjerkhoel.com/", 145 | "visits": 1500, 146 | "last_visited": "2011-11-261T00:00:01Z" 147 | } 148 | ] 149 | }, { 150 | "id": 2, 151 | "name": "John Doe", 152 | "body": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod.", 153 | "url": "http://example.com/users/john-doe", 154 | "links": [ 155 | { 156 | "url": "http://google.com/", 157 | "visits": 11000, 158 | "last_visited": "2010-05-221T00:00:01Z" 159 | }, 160 | { 161 | "url": "http://twitter.com/", 162 | "visits": 155012857, 163 | "last_visited": "2011-11-261T00:00:01Z" 164 | } 165 | ] 166 | } 167 | ] 168 | } 169 | ``` 170 | 171 | ### Including JSONP callbacks 172 | 173 | Out of the box JSON Builder supports JSONP callbacks when used within a Rails project just by using the `callback` parameter. For instance, if you requested `/users.json?callback=myjscallback`, you'll get a callback wrapping the response: 174 | 175 | ```json 176 | myjscallback([ 177 | { 178 | "name": "Garrett Bjerkhoel" 179 | }, 180 | { 181 | "name": "John Doe" 182 | } 183 | ]) 184 | ``` 185 | 186 | To turn off JSONP callbacks globally or just per-environment: 187 | 188 | 189 | #### Globally 190 | 191 | ```ruby 192 | ActionView::Base.json_callback = false 193 | ``` 194 | 195 | #### Per Environment 196 | 197 | ```ruby 198 | Sample::Application.configure do 199 | config.action_view.json_callback = false 200 | end 201 | ``` 202 | 203 | ### Pretty Print Output 204 | 205 | Out of the box JSON Builder supports pretty printing only during development, it's disabled by default in other environments for performance. If you'd like to enable or disable pretty printing you can do it within your environment file or you can do it globally. 206 | 207 | With pretty print on: 208 | 209 | ```json 210 | { 211 | "name": "Garrett Bjerkhoel", 212 | "email": "spam@garrettbjerkhoel.com" 213 | } 214 | ``` 215 | 216 | Without: 217 | 218 | ```json 219 | {"name": "Garrett Bjerkhoel", "email": "spam@garrettbjerkhoel.com"} 220 | ``` 221 | 222 | #### Per Environment 223 | 224 | ```ruby 225 | Sample::Application.configure do 226 | config.action_view.pretty_print_json = false 227 | end 228 | ``` 229 | 230 | #### Globally 231 | 232 | ```ruby 233 | ActionView::Base.pretty_print_json = false 234 | ``` 235 | 236 | ## Speed 237 | JSON Builder is very fast, it's roughly 3.6 times faster than the core XML Builder based on the [speed benchmark](http://github.com/dewski/json_builder/blob/master/spec/benchmarks/builder.rb). 238 | 239 | user system total real 240 | JSONBuilder 2.950000 0.010000 2.960000 (2.968790) 241 | Builder 10.820000 0.040000 10.860000 (10.930497) 242 | 243 | ## Alternative libraries 244 | 245 | There are alternatives to JSON Builder, each good in their own way with different API's and design approaches that are worth checking out. Although, I would love to hear why JSON Builder didn't fit your needs, by [message or issue. 246 | 247 | * [jbuilder](https://github.com/rails/jbuilder) 248 | * [RABL](https://github.com/nesquena/rabl) 249 | * [Tequila](https://github.com/inem/tequila) 250 | * [Argonaut](https://github.com/jbr/argonaut) 251 | * [Jsonify](https://github.com/bsiggelkow/jsonify) 252 | * [RepresentationView](https://github.com/mdub/representative_view) 253 | 254 | ## Note on Patches/Pull Requests 255 | 256 | - Fork the project. 257 | - Make your feature addition or bug fix. 258 | - Add tests for it. This is important so I don't break it in a future version unintentionally. 259 | - Commit, do not mess with Rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull) 260 | - Send me a pull request. Bonus points for topic branches. 261 | 262 | ## Copyright 263 | Copyright © 2012 Garrett Bjerkhoel. See [MIT-LICENSE](http://github.com/dewski/json_builder/blob/master/MIT-LICENSE) for details. 264 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | require 'rake/testtask' 4 | begin 5 | require 'bundler/setup' 6 | Bundler::GemHelper.install_tasks 7 | rescue LoadError 8 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 9 | end 10 | 11 | desc 'Default: run tests' 12 | task :default => :test 13 | 14 | desc 'Run JSONBuilder tests.' 15 | Rake::TestTask.new(:test) do |t| 16 | t.libs << 'lib' 17 | t.libs << 'test' 18 | t.test_files = FileList['test/*_test.rb'] 19 | t.verbose = true 20 | end 21 | -------------------------------------------------------------------------------- /json_builder.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | require 'json_builder/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'json_builder' 6 | s.version = JSONBuilder::VERSION 7 | s.summary = 'Rails provides an excellent XML Builder by default to build RSS and ATOM feeds, but nothing to help you build complex and custom JSON data structures. The standard to_json works well, but can get very verbose when you need full control of what is generated. JSON Builder hopes to solve that problem.' 8 | s.description = 'Rails provides an excellent XML Builder by default to build RSS and ATOM feeds, but nothing to help you build complex and custom JSON data structures. The standard to_json works well, but can get very verbose when you need full control of what is generated. JSON Builder hopes to solve that problem.' 9 | s.authors = ['Garrett Bjerkhoel'] 10 | s.email = ['me@garrettbjerkhoel.com'] 11 | s.platform = Gem::Platform::RUBY 12 | 13 | s.files = `git ls-files`.split("\n") 14 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 15 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 16 | s.require_paths = ['lib'] 17 | 18 | s.add_dependency 'activesupport', '>= 2.0.0' 19 | s.add_dependency 'json' 20 | 21 | s.add_development_dependency 'tzinfo' 22 | end 23 | -------------------------------------------------------------------------------- /lib/json_builder.rb: -------------------------------------------------------------------------------- 1 | require 'json_builder/version' 2 | require 'json_builder/compiler' 3 | require 'json_builder/template' if defined? Rails 4 | 5 | module JSONBuilder 6 | class InvalidArgument < StandardError; end 7 | class MissingKeyError < StandardError; end 8 | end 9 | -------------------------------------------------------------------------------- /lib/json_builder/compiler.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'json_builder/member' 3 | 4 | module JSONBuilder 5 | class Compiler 6 | class << self 7 | # Public: The helper that builds the JSON structure by calling the 8 | # specific methods needed to build the JSON. 9 | # 10 | # args - Any number of arguments needed for the JSONBuilder::Compiler. 11 | # block - Yielding a block to generate the JSON. 12 | # 13 | # Returns a String. 14 | def generate(*args, &block) 15 | options = args.extract_options! 16 | compiler = self.new(options) 17 | compiler.compile(*args, &block) 18 | compiler.finalize 19 | end 20 | end 21 | 22 | attr_accessor :members 23 | attr_accessor :array 24 | attr_accessor :scope 25 | attr_accessor :callback 26 | attr_accessor :pretty_print 27 | 28 | # Needed to allow for the id key to be used 29 | undef_method :id if methods.include? 'id' 30 | 31 | # Public: Creates a new Compiler instance used to hold any and 32 | # all JSONBuilder::Member objects. 33 | # 34 | # options - Hash of options used to modify JSON output. 35 | # 36 | # Examples 37 | # 38 | # json = JSONBuilder::Compiler.new(:callback => false) 39 | # json.compile do 40 | # name 'Garrett' 41 | # end 42 | # json.finalize 43 | # # => {"name": "Garrett"} 44 | # 45 | # Returns instance of JSONBuilder::Compiler. 46 | def initialize(options={}) 47 | @_members = [] 48 | @_scope = options.fetch(:scope, nil) 49 | @_callback = options.fetch(:callback, true) 50 | @_pretty_print = options.fetch(:pretty, false) 51 | 52 | # Only copy instance variables if there is a scope and presence of Rails 53 | copy_instance_variables_from(@_scope) if @_scope 54 | end 55 | 56 | # Public: Takes a block to generate the JSON structure by calling method_missing 57 | # on all members passed to it through the block. 58 | # 59 | # args - An array of values passed to JSONBuilder::Value. 60 | # block - Yielding a block to generate the JSON. 61 | # 62 | # Returns nothing. 63 | def compile(*args, &block) 64 | instance_exec(*args, &block) 65 | end 66 | 67 | # Public: Takes a set number of items to generate a plain JSON array response. 68 | # 69 | # Returns instance of JSONBuilder::Elements. 70 | def array(items, &block) 71 | @_array = Elements.new(@_scope, items, &block) 72 | end 73 | 74 | # Public: Called anytime the compiler is passed JSON keys, 75 | # first checks to see if the parent object contains the method like 76 | # a Rails helper. 77 | # 78 | # key_name - The key for the JSON member. 79 | # args - An array of values passed to JSONBuilder::Value. 80 | # block - Yielding any block passed to the element. 81 | # 82 | # Returns nothing. 83 | def method_missing(key_name, *args, &block) 84 | if @_scope.respond_to?(key_name) && !ignore_scope_methods.include?(key_name) 85 | @_scope.send(key_name, *args, &block) 86 | else 87 | key(key_name, *args, &block) 88 | end 89 | end 90 | 91 | # Public: Generates the start of the JSON member. Useful if the key you are 92 | # generating is dynamic. 93 | # 94 | # key - Used to generate the JSON member's key. Can be a String or Symbol. 95 | # args - An array of values passed to JSONBuilder::Value. 96 | # block - Yielding any block passed to the element. 97 | # 98 | # Examples 99 | # 100 | # key :hello, 'Hi' 101 | # # => "hello": "Hi" 102 | # 103 | # key "item-#{rand(0, 500)}", "I'm random!" 104 | # # => "item-250": "I'm random!" 105 | # 106 | # Returns instance of JSONBuilder::Member. 107 | def key(key_name, *args, &block) 108 | member = Member.new(key_name, @_scope, *args, &block) 109 | @_members << member 110 | member 111 | end 112 | 113 | # Public: Combines the output of the compiled members and the change 114 | # there is a JSONP callback. This is what is returned in the response. 115 | # 116 | # Returns a String. 117 | def finalize 118 | include_callback to_s 119 | end 120 | 121 | # Public: Gathers the JSON structure and calls it's compiler within each 122 | # instance. 123 | # 124 | # Returns a String. 125 | def to_s 126 | @_array ? @_array.to_s : "{#{@_members.collect(&:to_s).join(', ')}}" 127 | end 128 | 129 | private 130 | 131 | # Private: Determines whether or not to include a JSONP callback in 132 | # the response. 133 | # 134 | # json - The String representation of the JSON structure. 135 | # 136 | # Returns a String. 137 | def include_callback(json) 138 | @_callback && request_params[:callback] ? "#{request_params[:callback]}(#{pretty_print(json)})" : pretty_print(json) 139 | end 140 | 141 | # Private: Determines whether or not to pass the string through the a 142 | # JSON prettifier to help with debugging. 143 | # 144 | # json - The String representation of the JSON structure. 145 | # 146 | # Returns a String. 147 | def pretty_print(json) 148 | @_pretty_print ? JSON.pretty_generate(JSON[json]) : json 149 | end 150 | 151 | # Private: Contains the params from the request. 152 | # 153 | # Returns a Hash. 154 | def request_params 155 | @_scope.respond_to?(:params) ? @_scope.params : {} 156 | end 157 | 158 | # Private: Takes all instance variables from the scope passed to it 159 | # and makes them available to the block that gets compiled. 160 | # 161 | # object - The scope which contains the instance variables. 162 | # exclude - Any instance variables that should not be set. 163 | # 164 | # Returns nothing. 165 | def copy_instance_variables_from(object, exclude = []) #:nodoc: 166 | vars = object.instance_variables.map(&:to_s) - exclude.map(&:to_s) 167 | vars.each { |name| instance_variable_set(name.to_sym, object.instance_variable_get(name)) } 168 | end 169 | 170 | # Private: Array of instance variable names that should not be set for 171 | # the scope. 172 | # 173 | # Returns an Array of Symbols. 174 | def ignore_scope_methods 175 | [:id] 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/json_builder/elements.rb: -------------------------------------------------------------------------------- 1 | module JSONBuilder 2 | class Elements 3 | attr_accessor :compilers 4 | 5 | # Public: Creates a new instance of the Elements that generates 6 | # an array of JSONBuilder::Member objects. 7 | # 8 | # scope - The view scope context for any variables. 9 | # items - The array of elements to create values from. 10 | # block - Yielding any block passed to the element. 11 | # 12 | # Raises InvalidArgument if the items passed does not respond to each. 13 | # Returns a new instance of Elements. 14 | def initialize(scope, items, &block) 15 | raise InvalidArgument.new('items does not respond to each') unless items.respond_to?(:each) 16 | 17 | @compilers = [] 18 | 19 | items.each do |item| 20 | @compilers << Value.new(scope, item, &block) 21 | end 22 | end 23 | 24 | # Public: Generates the array JSON block local values 25 | # 26 | # Returns a formated JSON String 27 | def to_s 28 | "[#{@compilers.collect(&:to_s).join(', ')}]" 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/json_builder/extensions.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/all' 2 | 3 | class FalseClass 4 | def to_builder 5 | 'false' 6 | end 7 | end 8 | 9 | class TrueClass 10 | def to_builder 11 | 'true' 12 | end 13 | end 14 | 15 | class String 16 | JS_ESCAPE_MAP = { 17 | '\\' => '\\\\', 18 | ' '<\/', 19 | "\r\n" => '\n', 20 | "\n" => '\n', 21 | "\r" => '\n', 22 | "\t" => '\t', 23 | '"' => '\\"', 24 | "'" => "\'" 25 | } 26 | 27 | def to_builder 28 | %("#{json_escape}") 29 | end 30 | 31 | private 32 | 33 | def json_escape 34 | gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\t\n\r"'])/u) { |match| 35 | JS_ESCAPE_MAP[match] 36 | } 37 | end 38 | end 39 | 40 | class Hash 41 | def to_builder 42 | to_json 43 | end 44 | end 45 | 46 | class NilClass 47 | def to_builder 48 | 'null' 49 | end 50 | end 51 | 52 | module ActiveSupport 53 | class TimeWithZone 54 | def to_builder 55 | %("#{iso8601}") 56 | end 57 | end 58 | end 59 | 60 | class Time 61 | def to_builder 62 | %("#{iso8601}") 63 | end 64 | end 65 | 66 | class Date 67 | def to_builder 68 | %("#{to_time.iso8601}") 69 | end 70 | end 71 | 72 | class DateTime 73 | def to_builder 74 | %("#{to_time.iso8601}") 75 | end 76 | end 77 | 78 | # Mongoid < 3.0.0 79 | module BSON 80 | class ObjectId 81 | def to_builder 82 | %("#{self}") 83 | end 84 | end 85 | end 86 | 87 | # Mongoid >= 3.0.0 88 | module Moped 89 | module BSON 90 | class ObjectId 91 | def to_builder 92 | %("#{self}") 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/json_builder/member.rb: -------------------------------------------------------------------------------- 1 | require 'json_builder/value' 2 | require 'json_builder/elements' 3 | 4 | module JSONBuilder 5 | class Member 6 | attr_accessor :key, :value 7 | 8 | # Public: Returns a key value pair for the stored value which could 9 | # be an instance of JSONBuilder::Elements or JSONBuilder::Value. 10 | # 11 | # key - Used to generate the JSON member's key. Can be a String or Symbol. 12 | # scope - The view scope context for any variables. 13 | # args - Can be an Array or other standard Ruby value. 14 | # block - Yielding any block passed to the element. 15 | # 16 | # Raises JSONBuilder::MissingKeyError if the key passed is nil. 17 | # Returns instance of JSONBuilder::Member. 18 | def initialize(key, scope, *args, &block) 19 | raise MissingKeyError if key.nil? 20 | 21 | @key = key 22 | 23 | argument = args.shift 24 | if argument.is_a?(Enumerable) && !argument.is_a?(Hash) 25 | @value = Elements.new(scope, argument, &block) 26 | else 27 | @value = Value.new(scope, argument, &block) 28 | end 29 | end 30 | 31 | # Public: Returns a key value pair for the stored value which could 32 | # be an instance of JSONBuilder::Elements or JSONBuilder::Value. 33 | # 34 | # Returns a String. 35 | def to_s 36 | "\"#{@key}\": #{@value}" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/json_builder/template.rb: -------------------------------------------------------------------------------- 1 | module ActionView #:nodoc: 2 | class Base 3 | cattr_accessor :pretty_print_json 4 | @@pretty_print_json = defined?(Rails) && Rails.env.development? 5 | 6 | cattr_accessor :json_callback 7 | @@json_callback = true 8 | end 9 | end 10 | 11 | # Rails 2.X Template 12 | if defined?(Rails) && Rails.version.starts_with?('2') 13 | require 'action_view/base' 14 | require 'action_view/template' 15 | 16 | module ActionView 17 | module TemplateHandlers 18 | class JSONBuilder < TemplateHandler 19 | include Compilable 20 | 21 | def compile(template) %{ 22 | ::JSONBuilder::Compiler.generate(:scope => self, :pretty => ActionView::Base.pretty_print_json, :callback => ActionView::Base.json_callback) { 23 | #{template.source} 24 | } 25 | } end 26 | end 27 | end 28 | end 29 | 30 | ActionView::Template.register_template_handler :json_builder, ActionView::TemplateHandlers::JSONBuilder 31 | end 32 | 33 | # Rails 3.X and 4.X Template 34 | if defined?(Rails) && Rails.version.starts_with?('3', '4') 35 | module ActionView 36 | module Template::Handlers 37 | class JSONBuilder 38 | class_attribute :default_format 39 | self.default_format = Mime::JSON 40 | 41 | def self.call(template) 42 | source = if template.source.empty? 43 | File.read(template.identifier) 44 | else # use source 45 | template.source 46 | end 47 | 48 | %{ 49 | ::JSONBuilder::Compiler.generate(:scope => self, :pretty => ActionView::Base.pretty_print_json, :callback => ActionView::Base.json_callback) { 50 | #{source} 51 | } 52 | } 53 | end 54 | end 55 | end 56 | end 57 | 58 | ActionView::Template.register_template_handler :json_builder, ActionView::Template::Handlers::JSONBuilder 59 | end 60 | -------------------------------------------------------------------------------- /lib/json_builder/value.rb: -------------------------------------------------------------------------------- 1 | require 'json_builder/extensions' 2 | 3 | module JSONBuilder 4 | class Value 5 | attr_accessor :value 6 | 7 | # Public: Creates 8 | # 9 | # scope - The view scope context for any variables. 10 | # arg - Could be string, hash, or any other Ruby value. 11 | # block - Yielding any block passed to the element. 12 | # 13 | # Returns an instance of JSONBuilder::Member, JSONBuilder::Compiler 14 | # or String. 15 | def initialize(scope, arg, &block) 16 | if block_given? 17 | @value = Compiler.new(:scope => scope) 18 | compiled = @value.compile(arg, &block) 19 | 20 | # For the use case that the passed in block returns a non-member object 21 | # or normal Ruby object 22 | @value = compiled unless compiled.is_a?(Member) 23 | else 24 | @value = arg 25 | end 26 | end 27 | 28 | # Public: Determines of the stored value has a special return value 29 | # or calls the default to_s on it. 30 | # 31 | # Returns a String. 32 | def to_s 33 | @value.respond_to?(:to_builder) ? @value.to_builder : @value.to_s 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/json_builder/version.rb: -------------------------------------------------------------------------------- 1 | module JSONBuilder 2 | VERSION = '3.1.7' 3 | end 4 | -------------------------------------------------------------------------------- /test/benchmarks/builder.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'benchmark' 3 | require 'builder' 4 | require 'json_builder' 5 | 6 | Benchmark.bm do |b| 7 | b.report('JSONBuilder') do 8 | 15_000.times { 9 | JSONBuilder::Compiler.generate { 10 | name "Garrett Bjerkhoel" 11 | birthday Time.local(1991, 9, 14) 12 | street do 13 | address "1143 1st Ave" 14 | address2 "Apt 200" 15 | city "New York" 16 | state "New York" 17 | zip 10065 18 | end 19 | skills do 20 | ruby true 21 | asp false 22 | php true 23 | mysql true 24 | mongodb true 25 | haproxy true 26 | marathon false 27 | end 28 | single_skills ['ruby', 'php', 'mysql', 'mongodb', 'haproxy'] 29 | booleans [true, true, false, nil] 30 | } 31 | } 32 | end 33 | b.report('Builder') do 34 | 15_000.times { 35 | xml = Builder::XmlMarkup.new(:indent => 2) 36 | xml.name "Garrett Bjerkhoel" 37 | xml.birthday Time.local(1991, 9, 14) 38 | xml.street do 39 | xml.address "1143 1st Ave" 40 | xml.address2 "Apt 200" 41 | xml.city "New York" 42 | xml.state "New York" 43 | xml.zip 10065 44 | end 45 | xml.skills do 46 | xml.ruby true 47 | xml.asp false 48 | xml.php true 49 | xml.mysql true 50 | xml.mongodb true 51 | xml.haproxy true 52 | xml.marathon false 53 | end 54 | xml.single_skills ['ruby', 'php', 'mysql', 'mongodb', 'haproxy'] 55 | xml.booleans [true, true, false, nil] 56 | xml.target! 57 | } 58 | end 59 | end -------------------------------------------------------------------------------- /test/compiler_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | require 'active_support/ordered_hash' 5 | 6 | class TestCompiler < Test::Unit::TestCase 7 | def assert_builder_json(json, *args, &block) 8 | assert_equal json, JSONBuilder::Compiler.generate(*args, &block) 9 | end 10 | 11 | def test_without_nesting 12 | assert_builder_json('{"name": "Garrett Bjerkhoel", "valid": true}') do 13 | def valid? 14 | true 15 | end 16 | 17 | name 'Garrett Bjerkhoel' 18 | valid valid? 19 | end 20 | end 21 | 22 | def test_support_all_dates 23 | actual = JSONBuilder::Compiler.generate do 24 | date Date.new(2011, 11, 23) 25 | date_time DateTime.new(2001, 2, 3, 4, 5, 6) 26 | timed Time.utc(2012) 27 | Time.zone = "CET" 28 | zoned Time.zone.local(2012) 29 | end 30 | # The date will have the local time zone offset, hence the wildcard. 31 | assert_match(%r{\{"date": "2011-11-23T00:00:00.*", "date_time": "2001-02-03T04:05:06Z", "timed": "2012-01-01T00:00:00Z", "zoned": "2012-01-01T00:00:00\+01:00"\}}, actual) 32 | end 33 | 34 | def test_should_support_all_datatypes 35 | assert_builder_json('{"integer": 1, "mega_integer": 100000000, "float": 13.37, "true_class": true, "false_class": false, "missing_nil": null}') do 36 | integer 1 37 | mega_integer 100_000_000 38 | float 13.37 39 | true_class true 40 | false_class false 41 | missing_nil 42 | end 43 | end 44 | 45 | def test_should_support_multiple_nestings 46 | assert_builder_json('{"u": [{"id": 1, "l": [{"l": 1, "d": "t"}, {"l": 2, "d": "tt"}]}, {"id": 2, "l": [{"l": 2, "d": "t"}, {"l": 4, "d": "tt"}]}]}') do 47 | u [1, 2] do |i| 48 | id i 49 | l [1, 2] do |b| 50 | l b * i 51 | d 't' do |c| 52 | c * b 53 | end 54 | end 55 | end 56 | end 57 | end 58 | 59 | def test_support_custom_key_names 60 | assert_builder_json('{"custom_key": 1, "with_method": "nope", "as_string": true, "nested": {"deep_down": -1, "custom": true}, "nope": "chuck"}') do 61 | def with_method 62 | "nope" 63 | end 64 | 65 | key :custom_key, 1 66 | key :with_method, with_method 67 | key 'as_string', true 68 | nested do 69 | def custom 70 | 'custom' 71 | end 72 | 73 | key 'deep_down', -1 74 | key custom, true 75 | end 76 | key with_method, 'chuck' 77 | end 78 | end 79 | 80 | def test_support_custom_classes 81 | assert_builder_json('{"hello": "olleh"}') do 82 | hello Dozer.new('hello') 83 | end 84 | end 85 | 86 | def test_adding_hash_objects 87 | assert_builder_json('{"hash_test": {"garrett":true}}') do 88 | hash_test :garrett => true 89 | end 90 | end 91 | 92 | def test_adding_unicoded_key 93 | assert_builder_json('{"é": "json"}') do 94 | key 'é', 'json' 95 | end 96 | end 97 | 98 | def test_newline_characters 99 | assert_builder_json('{"newline": "hello\nworld"}') do 100 | newline "hello\nworld" 101 | end 102 | end 103 | 104 | def test_tab_characters 105 | assert_builder_json('{"tab": "hello\tworld"}') do 106 | tab "hello\tworld" 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/elements_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestElements < Test::Unit::TestCase 4 | def assert_elements_equal(value, array) 5 | assert_equal value, JSONBuilder::Elements.new(nil, array).to_s 6 | end 7 | 8 | def test_array_hash 9 | assert_elements_equal '[{"woot":true}]', [{ :woot => true }] 10 | end 11 | 12 | def test_custom_class_objects 13 | assert_elements_equal '["olleh", "eybdoog"]', [Dozer.new('hello'), Dozer.new('goodbye')] 14 | end 15 | 16 | def test_raises_invalid_argument 17 | assert_raises(JSONBuilder::InvalidArgument) { 18 | JSONBuilder::Elements.new(nil, false).to_s 19 | } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/extensions_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestExtensions < Test::Unit::TestCase 4 | def test_string_respond_to 5 | assert_respond_to 'json_builder', :to_builder 6 | end 7 | 8 | def test_ordered_hash 9 | assert_respond_to ActiveSupport::OrderedHash.new(:json_builder => true), :to_builder 10 | end 11 | 12 | def test_true_value 13 | assert_respond_to true, :to_builder 14 | end 15 | 16 | def test_false_value 17 | assert_respond_to false, :to_builder 18 | end 19 | 20 | def test_hash_value 21 | assert_respond_to({ :json_builder => true }, :to_builder) 22 | end 23 | 24 | def test_nil_value 25 | assert_respond_to nil, :to_builder 26 | end 27 | 28 | def test_time_with_zone_value 29 | assert_respond_to Time.zone.now, :to_builder 30 | end 31 | 32 | def test_time_value 33 | assert_respond_to Time.utc(2012), :to_builder 34 | end 35 | 36 | def test_date_value 37 | assert_respond_to Date.parse('2012-01-01'), :to_builder 38 | end 39 | 40 | def test_datetime_value 41 | assert_respond_to DateTime.parse('2012-01-01'), :to_builder 42 | end 43 | 44 | def test_bson_objectid_value 45 | assert_respond_to BSON::ObjectId.new, :to_builder 46 | end 47 | 48 | def test_moped_bson_objectid_value 49 | assert_respond_to Moped::BSON::ObjectId.new, :to_builder 50 | end 51 | 52 | def test_custom_class 53 | assert_respond_to Dozer.new('hello'), :to_builder 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/member_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | class TestMember < Test::Unit::TestCase 6 | def member(key, value=nil, &block) 7 | JSONBuilder::Member.new(key, nil, value, &block) 8 | end 9 | 10 | def test_is_a_builder_value 11 | assert_equal JSONBuilder::Member, member(:hello, true).class 12 | end 13 | 14 | def test_key_as_symbol 15 | assert_equal '"hello": true', member(:hello, true).to_s 16 | end 17 | 18 | def test_key_as_unicoded_symbol 19 | assert_equal '"hellyé": true', member('hellyé', true).to_s 20 | end 21 | 22 | def test_key_as_string 23 | assert_equal '"hello": true', member('hello', true).to_s 24 | end 25 | 26 | def test_value_as_array 27 | assert_equal '"hello": [{"ruby":true}]', member('hello', [{ :ruby => true }]).to_s 28 | end 29 | 30 | def test_value_as_block 31 | assert_equal '"hello": "hi"', member('hello') { 'hi' }.to_s 32 | end 33 | 34 | def test_value_as_block_with_hash 35 | assert_equal '"hello": {"ruby":true}', member('hello') { { :ruby => true } }.to_s 36 | end 37 | 38 | def test_custom_class 39 | assert_equal '"hello": "olleh"', member('hello', Dozer.new('hello')).to_s 40 | end 41 | 42 | def test_double_quoted_value 43 | assert_equal '"hello": "\"Hello\" he said"', member('hello', '"Hello" he said').to_s 44 | end 45 | 46 | def test_single_quoted_value 47 | assert_equal %Q("hello": "hello 'test'!"), member('hello', "hello 'test'!").to_s 48 | end 49 | 50 | def test_without_key 51 | assert_raises(JSONBuilder::MissingKeyError) { member(nil, true).to_s } 52 | end 53 | 54 | def test_with_enumerable 55 | json = member('hello', [1, 2, 3]) { |i| index i }.to_s 56 | 57 | assert_equal '"hello": [{"index": 1}, {"index": 2}, {"index": 3}]', json 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | Bundler.setup(:default, :test) 4 | Bundler.require(:default, :test) 5 | 6 | require 'coveralls' 7 | Coveralls.wear! 8 | 9 | dir = File.dirname(File.expand_path(__FILE__)) 10 | $LOAD_PATH.unshift dir + '/../lib' 11 | $TESTING = true 12 | require 'test/unit' 13 | require 'json_builder' 14 | require 'tzinfo' 15 | 16 | class Dozer 17 | attr_accessor :value 18 | 19 | def initialize(value) 20 | @value = value 21 | end 22 | 23 | def to_builder 24 | @value.reverse.inspect 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/value_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | class TestValue < Test::Unit::TestCase 6 | def value(value) 7 | JSONBuilder::Value.new(nil, value).to_s 8 | end 9 | 10 | def test_positive_value 11 | assert_equal '1', value(1) 12 | end 13 | 14 | def test_negative_value 15 | assert_equal '-5', value(-5) 16 | end 17 | 18 | def test_float_value 19 | assert_equal '13.37', value(13.37) 20 | end 21 | 22 | def test_nil_value 23 | assert_equal 'null', value(nil) 24 | end 25 | 26 | def test_true_value 27 | assert_equal 'true', value(true) 28 | end 29 | 30 | def test_false_value 31 | assert_equal 'false', value(false) 32 | end 33 | 34 | def test_symbol_value 35 | assert_equal 'test', value(:test) 36 | end 37 | 38 | def test_unicode_char_value 39 | assert_equal '"hellyé"', value('hellyé') 40 | end 41 | 42 | def test_time_value 43 | assert_equal '"2012-01-01T00:00:00Z"', value(Time.utc(2012)) 44 | end 45 | 46 | def test_time_with_zone_value 47 | Time.zone = 'CET' 48 | assert_equal '"2012-01-01T00:00:00+01:00"', value(Time.zone.local(2012)) 49 | end 50 | 51 | # This will be the local time zone offset, hence the wildcard. 52 | def test_date_value 53 | assert_match /"2012-01-01T00:00:00.*/, value(Date.parse('2012-01-01')) 54 | end 55 | 56 | def test_date_time_value 57 | assert_equal '"2012-01-01T00:00:00Z"', value(DateTime.parse('2012-01-01')) 58 | end 59 | 60 | def test_hash_value 61 | assert_equal '{"oh":"boy"}', value(:oh => :boy) 62 | end 63 | 64 | def test_custom_class 65 | assert_equal '"olleh"', value(Dozer.new('hello')) 66 | end 67 | 68 | def test_double_quoted_value 69 | assert_equal '"\"hello\""', value('"hello"') 70 | end 71 | end 72 | --------------------------------------------------------------------------------