├── .gitignore ├── Gemfile ├── lib ├── generators │ └── rails │ │ ├── templates │ │ ├── show.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── controller.rb │ │ ├── scaffold_controller_generator.rb │ │ └── jbuilder_generator.rb ├── jbuilder │ ├── jbuilder.rb │ ├── errors.rb │ ├── railtie.rb │ ├── key_formatter.rb │ ├── dependency_tracker.rb │ └── jbuilder_template.rb └── jbuilder.rb ├── test ├── test_helper.rb ├── jbuilder_generator_test.rb ├── jbuilder_dependency_tracker_test.rb ├── scaffold_controller_generator_test.rb ├── jbuilder_template_test.rb └── jbuilder_test.rb ├── gemfiles ├── rails_3_0.gemfile ├── rails_3_1.gemfile ├── rails_3_2.gemfile ├── rails_4_0.gemfile └── rails_4_1.gemfile ├── .travis.yml ├── Appraisals ├── Rakefile ├── jbuilder.gemspec ├── MIT-LICENSE ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | gemfiles/*.lock 3 | Gemfile.lock 4 | .ruby-version 5 | pkg 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "mocha", require: false 7 | gem "appraisal" 8 | -------------------------------------------------------------------------------- /lib/generators/rails/templates/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @<%= singular_table_name %>, <%= attributes_list_with_timestamps %> 2 | -------------------------------------------------------------------------------- /lib/jbuilder/jbuilder.rb: -------------------------------------------------------------------------------- 1 | Jbuilder = Class.new(begin 2 | require 'active_support/proxy_object' 3 | ActiveSupport::ProxyObject 4 | rescue LoadError 5 | require 'active_support/basic_object' 6 | ActiveSupport::BasicObject 7 | end) 8 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "rails/version" 3 | 4 | if Rails::VERSION::STRING > "4.0" 5 | require "active_support/testing/autorun" 6 | else 7 | require "test/unit" 8 | end 9 | 10 | require "active_support/test_case" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_3_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake" 6 | gem "mocha", :require => false 7 | gem "appraisal" 8 | gem "railties", "~> 3.0.0" 9 | gem "actionpack", "~> 3.0.0" 10 | 11 | gemspec :path => "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_3_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake" 6 | gem "mocha", :require => false 7 | gem "appraisal" 8 | gem "railties", "~> 3.1.0" 9 | gem "actionpack", "~> 3.1.0" 10 | 11 | gemspec :path => "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_3_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake" 6 | gem "mocha", :require => false 7 | gem "appraisal" 8 | gem "railties", "~> 3.2.0" 9 | gem "actionpack", "~> 3.2.0" 10 | 11 | gemspec :path => "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_4_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake" 6 | gem "mocha", :require => false 7 | gem "appraisal" 8 | gem "railties", "~> 4.0.0" 9 | gem "actionpack", "~> 4.0.0" 10 | 11 | gemspec :path => "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_4_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake" 6 | gem "mocha", :require => false 7 | gem "appraisal" 8 | gem "railties", "~> 4.1.0" 9 | gem "actionpack", "~> 4.1.0" 10 | 11 | gemspec :path => "../" 12 | -------------------------------------------------------------------------------- /lib/generators/rails/templates/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(@<%= plural_table_name %>) do |<%= singular_table_name %>| 2 | json.extract! <%= singular_table_name %>, <%= attributes_list %> 3 | json.url <%= singular_table_name %>_url(<%= singular_table_name %>, format: :json) 4 | end 5 | -------------------------------------------------------------------------------- /lib/jbuilder/errors.rb: -------------------------------------------------------------------------------- 1 | require 'jbuilder/jbuilder' 2 | 3 | class Jbuilder 4 | class NullError < ::NoMethodError 5 | def self.build(key) 6 | message = "Failed to add #{key.to_s.inspect} property to null object" 7 | new(message) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/rails/scaffold_controller_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' 3 | 4 | module Rails 5 | module Generators 6 | class ScaffoldControllerGenerator 7 | source_root File.expand_path('../templates', __FILE__) 8 | 9 | hook_for :jbuilder, default: true 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 1.9 5 | - 2.0 6 | - 2.1 7 | - 2.2 8 | - ruby-head 9 | - jruby-19mode 10 | - rbx 11 | 12 | gemfile: 13 | - gemfiles/rails_3_0.gemfile 14 | - gemfiles/rails_3_1.gemfile 15 | - gemfiles/rails_3_2.gemfile 16 | - gemfiles/rails_4_0.gemfile 17 | - gemfiles/rails_4_1.gemfile 18 | 19 | matrix: 20 | allow_failures: 21 | - rvm: 2.2 22 | - rvm: jruby-19mode 23 | - rvm: rbx 24 | - rvm: ruby-head 25 | fast_finish: true 26 | 27 | notifications: 28 | email: false 29 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-3-0" do 2 | gem "railties", "~> 3.0.0" 3 | gem "actionpack", "~> 3.0.0" 4 | end 5 | 6 | appraise "rails-3-1" do 7 | gem "railties", "~> 3.1.0" 8 | gem "actionpack", "~> 3.1.0" 9 | end 10 | 11 | appraise "rails-3-2" do 12 | gem "railties", "~> 3.2.0" 13 | gem "actionpack", "~> 3.2.0" 14 | end 15 | 16 | appraise "rails-4-0" do 17 | gem "railties", "~> 4.0.0" 18 | gem "actionpack", "~> 4.0.0" 19 | end 20 | 21 | appraise "rails-4-1" do 22 | gem "railties", "~> 4.1.0" 23 | gem "actionpack", "~> 4.1.0" 24 | end 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "bundler/gem_tasks" 3 | require "rake/testtask" 4 | 5 | if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"] 6 | require "appraisal/task" 7 | Appraisal::Task.new 8 | task default: :appraisal 9 | else 10 | Rake::TestTask.new do |test| 11 | require "rails/version" 12 | 13 | test.libs << "test" 14 | 15 | if Rails::VERSION::MAJOR == 3 16 | test.test_files = %w[test/jbuilder_template_test.rb test/jbuilder_test.rb] 17 | else 18 | test.test_files = FileList["test/*_test.rb"] 19 | end 20 | end 21 | 22 | task default: :test 23 | end 24 | -------------------------------------------------------------------------------- /jbuilder.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'jbuilder' 3 | s.version = '2.2.6' 4 | s.authors = ['David Heinemeier Hansson', 'Pavel Pravosud'] 5 | s.email = ['david@37signals.com', 'pavel@pravosud.com'] 6 | s.summary = 'Create JSON structures via a Builder-style DSL' 7 | s.homepage = 'https://github.com/rails/jbuilder' 8 | s.license = 'MIT' 9 | 10 | s.required_ruby_version = '>= 1.9.3' 11 | 12 | s.add_dependency 'activesupport', '>= 3.0.0', '< 5' 13 | s.add_dependency 'multi_json', '~> 1.2' 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- test/*`.split("\n") 17 | end 18 | -------------------------------------------------------------------------------- /lib/jbuilder/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails/railtie' 2 | require 'jbuilder/jbuilder_template' 3 | 4 | class Jbuilder 5 | class Railtie < ::Rails::Railtie 6 | initializer :jbuilder do |app| 7 | ActiveSupport.on_load :action_view do 8 | ActionView::Template.register_template_handler :jbuilder, JbuilderHandler 9 | require 'jbuilder/dependency_tracker' 10 | end 11 | end 12 | 13 | if Rails::VERSION::MAJOR == 4 14 | generators do |app| 15 | Rails::Generators.configure! app.config.generators 16 | Rails::Generators.hidden_namespaces.uniq! 17 | require 'generators/rails/scaffold_controller_generator' 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/jbuilder/key_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'jbuilder/jbuilder' 2 | require 'active_support/core_ext/array' 3 | 4 | class Jbuilder 5 | class KeyFormatter 6 | def initialize(*args) 7 | @format = {} 8 | @cache = {} 9 | 10 | options = args.extract_options! 11 | args.each do |name| 12 | @format[name] = [] 13 | end 14 | options.each do |name, paramaters| 15 | @format[name] = paramaters 16 | end 17 | end 18 | 19 | def initialize_copy(original) 20 | @cache = {} 21 | end 22 | 23 | def format(key) 24 | @cache[key] ||= @format.inject(key.to_s) do |result, args| 25 | func, args = args 26 | if ::Proc === func 27 | func.call result, *args 28 | else 29 | result.send func, *args 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2014 David Heinemeier Hansson, 37signals 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 | -------------------------------------------------------------------------------- /test/jbuilder_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/generators/test_case' 3 | require 'generators/rails/jbuilder_generator' 4 | 5 | class JbuilderGeneratorTest < Rails::Generators::TestCase 6 | tests Rails::Generators::JbuilderGenerator 7 | arguments %w(Post title body:text password:digest) 8 | destination File.expand_path('../tmp', __FILE__) 9 | setup :prepare_destination 10 | 11 | test 'views are generated' do 12 | run_generator 13 | 14 | %w(index show).each do |view| 15 | assert_file "app/views/posts/#{view}.json.jbuilder" 16 | end 17 | end 18 | 19 | test 'index content' do 20 | run_generator 21 | 22 | assert_file 'app/views/posts/index.json.jbuilder' do |content| 23 | assert_match /json\.array!\(@posts\) do \|post\|/, content 24 | assert_match /json\.extract! post, :id, :title, :body/, content 25 | assert_match /json\.url post_url\(post, format: :json\)/, content 26 | end 27 | 28 | assert_file 'app/views/posts/show.json.jbuilder' do |content| 29 | assert_match /json\.extract! @post, :id, :title, :body, :created_at, :updated_at/, content 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/generators/rails/jbuilder_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/named_base' 2 | require 'rails/generators/resource_helpers' 3 | 4 | module Rails 5 | module Generators 6 | class JbuilderGenerator < NamedBase # :nodoc: 7 | include Rails::Generators::ResourceHelpers 8 | 9 | source_root File.expand_path('../templates', __FILE__) 10 | 11 | argument :attributes, type: :array, default: [], banner: 'field:type field:type' 12 | 13 | def create_root_folder 14 | path = File.join('app/views', controller_file_path) 15 | empty_directory path unless File.directory?(path) 16 | end 17 | 18 | def copy_view_files 19 | %w(index show).each do |view| 20 | filename = filename_with_extensions(view) 21 | template filename, File.join('app/views', controller_file_path, filename) 22 | end 23 | end 24 | 25 | 26 | protected 27 | def attributes_names 28 | [:id] + super 29 | end 30 | 31 | def filename_with_extensions(name) 32 | [name, :json, :jbuilder] * '.' 33 | end 34 | 35 | def attributes_list_with_timestamps 36 | attributes_list(attributes_names + %w(created_at updated_at)) 37 | end 38 | 39 | def attributes_list(attributes = attributes_names) 40 | if self.attributes.any? {|attr| attr.name == 'password' && attr.type == :digest} 41 | attributes = attributes.reject {|name| %w(password password_confirmation).include? name} 42 | end 43 | 44 | attributes.map { |a| ":#{a}"} * ', ' 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/jbuilder/dependency_tracker.rb: -------------------------------------------------------------------------------- 1 | require 'jbuilder/jbuilder' 2 | 3 | dependency_tracker = false 4 | 5 | begin 6 | require 'action_view' 7 | require 'action_view/dependency_tracker' 8 | dependency_tracker = ::ActionView::DependencyTracker 9 | rescue LoadError 10 | begin 11 | require 'cache_digests' 12 | dependency_tracker = ::CacheDigests::DependencyTracker 13 | rescue LoadError 14 | end 15 | end 16 | 17 | if dependency_tracker 18 | class Jbuilder 19 | module DependencyTrackerMethods 20 | # Matches: 21 | # json.partial! "messages/message" 22 | # json.partial!('messages/message') 23 | # 24 | DIRECT_RENDERS = / 25 | \w+\.partial! # json.partial! 26 | \(?\s* # optional parenthesis 27 | (['"])([^'"]+)\1 # quoted value 28 | /x 29 | 30 | # Matches: 31 | # json.partial! partial: "comments/comment" 32 | # json.comments @post.comments, partial: "comments/comment", as: :comment 33 | # json.array! @posts, partial: "posts/post", as: :post 34 | # = render partial: "account" 35 | # 36 | INDIRECT_RENDERS = / 37 | (?::partial\s*=>|partial:) # partial: or :partial => 38 | \s* # optional whitespace 39 | (['"])([^'"]+)\1 # quoted value 40 | /x 41 | 42 | def dependencies 43 | direct_dependencies + indirect_dependencies + explicit_dependencies 44 | end 45 | 46 | private 47 | 48 | def direct_dependencies 49 | source.scan(DIRECT_RENDERS).map(&:second) 50 | end 51 | 52 | def indirect_dependencies 53 | source.scan(INDIRECT_RENDERS).map(&:second) 54 | end 55 | end 56 | end 57 | 58 | ::Jbuilder::DependencyTracker = Class.new(dependency_tracker::ERBTracker) 59 | ::Jbuilder::DependencyTracker.send :include, ::Jbuilder::DependencyTrackerMethods 60 | dependency_tracker.register_tracker :jbuilder, ::Jbuilder::DependencyTracker 61 | end 62 | -------------------------------------------------------------------------------- /test/jbuilder_dependency_tracker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'jbuilder/dependency_tracker' 3 | 4 | 5 | class FakeTemplate 6 | attr_reader :source, :handler 7 | def initialize(source, handler = :jbuilder) 8 | @source, @handler = source, handler 9 | end 10 | end 11 | 12 | 13 | class JbuilderDependencyTrackerTest < ActiveSupport::TestCase 14 | def make_tracker(name, source) 15 | template = FakeTemplate.new(source) 16 | Jbuilder::DependencyTracker.new(name, template) 17 | end 18 | 19 | def track_dependencies(source) 20 | make_tracker('jbuilder_template', source).dependencies 21 | end 22 | 23 | test 'detects dependency via direct partial! call' do 24 | dependencies = track_dependencies <<-RUBY 25 | json.partial! 'path/to/partial', foo: bar 26 | json.partial! 'path/to/another/partial', :fizz => buzz 27 | RUBY 28 | 29 | assert_equal %w[path/to/partial path/to/another/partial], dependencies 30 | end 31 | 32 | test 'detects dependency via direct partial! call with parens' do 33 | dependencies = track_dependencies <<-RUBY 34 | json.partial!("path/to/partial") 35 | RUBY 36 | 37 | assert_equal %w[path/to/partial], dependencies 38 | end 39 | 40 | test 'detects partial with options (1.9 style)' do 41 | dependencies = track_dependencies <<-RUBY 42 | json.partial! hello: 'world', partial: 'path/to/partial', foo: :bar 43 | RUBY 44 | 45 | assert_equal %w[path/to/partial], dependencies 46 | end 47 | 48 | test 'detects partial with options (1.8 style)' do 49 | dependencies = track_dependencies <<-RUBY 50 | json.partial! :hello => 'world', :partial => 'path/to/partial', :foo => :bar 51 | RUBY 52 | 53 | assert_equal %w[path/to/partial], dependencies 54 | end 55 | 56 | test 'detects partial in indirect collecton calls' do 57 | dependencies = track_dependencies <<-RUBY 58 | json.comments @post.comments, partial: 'comments/comment', as: :comment 59 | RUBY 60 | 61 | assert_equal %w[comments/comment], dependencies 62 | end 63 | 64 | test 'detects explicit depedency' do 65 | dependencies = track_dependencies <<-RUBY 66 | # Template Dependency: path/to/partial 67 | json.foo 'bar' 68 | RUBY 69 | 70 | assert_equal %w[path/to/partial], dependencies 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/scaffold_controller_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/generators/test_case' 3 | require 'generators/rails/scaffold_controller_generator' 4 | 5 | class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase 6 | tests Rails::Generators::ScaffoldControllerGenerator 7 | arguments %w(Post title body:text) 8 | destination File.expand_path('../tmp', __FILE__) 9 | setup :prepare_destination 10 | 11 | test 'controller content' do 12 | run_generator 13 | 14 | assert_file 'app/controllers/posts_controller.rb' do |content| 15 | assert_instance_method :index, content do |m| 16 | assert_match /@posts = Post\.all/, m 17 | end 18 | 19 | assert_instance_method :show, content do |m| 20 | assert m.blank? 21 | end 22 | 23 | assert_instance_method :new, content do |m| 24 | assert_match /@post = Post\.new/, m 25 | end 26 | 27 | assert_instance_method :edit, content do |m| 28 | assert m.blank? 29 | end 30 | 31 | assert_instance_method :create, content do |m| 32 | assert_match /@post = Post\.new\(post_params\)/, m 33 | assert_match /@post\.save/, m 34 | assert_match /format\.html \{ redirect_to @post, notice: 'Post was successfully created\.' \}/, m 35 | assert_match /format\.json \{ render :show, status: :created, location: @post \}/, m 36 | assert_match /format\.html \{ render :new \}/, m 37 | assert_match /format\.json \{ render json: @post\.errors, status: :unprocessable_entity \}/, m 38 | end 39 | 40 | assert_instance_method :update, content do |m| 41 | assert_match /format\.html \{ redirect_to @post, notice: 'Post was successfully updated\.' \}/, m 42 | assert_match /format\.json \{ render :show, status: :ok, location: @post \}/, m 43 | assert_match /format\.html \{ render :edit \}/, m 44 | assert_match /format\.json \{ render json: @post.errors, status: :unprocessable_entity \}/, m 45 | end 46 | 47 | assert_instance_method :destroy, content do |m| 48 | assert_match /@post\.destroy/, m 49 | assert_match /format\.html \{ redirect_to posts_url, notice: 'Post was successfully destroyed\.' \}/, m 50 | assert_match /format\.json \{ head :no_content \}/, m 51 | end 52 | 53 | assert_match(/def post_params/, content) 54 | assert_match(/params\.require\(:post\)\.permit\(:title, :body\)/, content) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/generators/rails/templates/controller.rb: -------------------------------------------------------------------------------- 1 | <% if namespaced? -%> 2 | require_dependency "<%= namespaced_file_path %>/application_controller" 3 | 4 | <% end -%> 5 | <% module_namespacing do -%> 6 | class <%= controller_class_name %>Controller < ApplicationController 7 | before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy] 8 | 9 | # GET <%= route_url %> 10 | # GET <%= route_url %>.json 11 | def index 12 | @<%= plural_table_name %> = <%= orm_class.all(class_name) %> 13 | end 14 | 15 | # GET <%= route_url %>/1 16 | # GET <%= route_url %>/1.json 17 | def show 18 | end 19 | 20 | # GET <%= route_url %>/new 21 | def new 22 | @<%= singular_table_name %> = <%= orm_class.build(class_name) %> 23 | end 24 | 25 | # GET <%= route_url %>/1/edit 26 | def edit 27 | end 28 | 29 | # POST <%= route_url %> 30 | # POST <%= route_url %>.json 31 | def create 32 | @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> 33 | 34 | respond_to do |format| 35 | if @<%= orm_instance.save %> 36 | format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> } 37 | format.json { render :show, status: :created, location: <%= "@#{singular_table_name}" %> } 38 | else 39 | format.html { render :new } 40 | format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity } 41 | end 42 | end 43 | end 44 | 45 | # PATCH/PUT <%= route_url %>/1 46 | # PATCH/PUT <%= route_url %>/1.json 47 | def update 48 | respond_to do |format| 49 | if @<%= orm_instance.update("#{singular_table_name}_params") %> 50 | format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> } 51 | format.json { render :show, status: :ok, location: <%= "@#{singular_table_name}" %> } 52 | else 53 | format.html { render :edit } 54 | format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity } 55 | end 56 | end 57 | end 58 | 59 | # DELETE <%= route_url %>/1 60 | # DELETE <%= route_url %>/1.json 61 | def destroy 62 | @<%= orm_instance.destroy %> 63 | respond_to do |format| 64 | format.html { redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %> } 65 | format.json { head :no_content } 66 | end 67 | end 68 | 69 | private 70 | # Use callbacks to share common setup or constraints between actions. 71 | def set_<%= singular_table_name %> 72 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> 73 | end 74 | 75 | # Never trust parameters from the scary internet, only allow the white list through. 76 | def <%= "#{singular_table_name}_params" %> 77 | <%- if attributes_names.empty? -%> 78 | params[<%= ":#{singular_table_name}" %>] 79 | <%- else -%> 80 | params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) 81 | <%- end -%> 82 | end 83 | end 84 | <% end -%> 85 | -------------------------------------------------------------------------------- /lib/jbuilder/jbuilder_template.rb: -------------------------------------------------------------------------------- 1 | require 'jbuilder/jbuilder' 2 | require 'action_dispatch/http/mime_type' 3 | require 'active_support/cache' 4 | 5 | class JbuilderTemplate < Jbuilder 6 | class << self 7 | attr_accessor :template_lookup_options 8 | end 9 | 10 | self.template_lookup_options = { handlers: [:jbuilder] } 11 | 12 | def initialize(context, *args, &block) 13 | @context = context 14 | super(*args, &block) 15 | end 16 | 17 | def partial!(name_or_options, locals = {}) 18 | case name_or_options 19 | when ::Hash 20 | # partial! partial: 'name', locals: { foo: 'bar' } 21 | options = name_or_options 22 | else 23 | # partial! 'name', foo: 'bar' 24 | options = { partial: name_or_options, locals: locals } 25 | as = locals.delete(:as) 26 | options[:as] = as if as.present? 27 | options[:collection] = locals[:collection] if locals.key?(:collection) 28 | end 29 | 30 | _render_partial_with_options options 31 | end 32 | 33 | def array!(collection = [], *attributes) 34 | options = attributes.extract_options! 35 | 36 | if options.key?(:partial) 37 | partial! options[:partial], options.merge(collection: collection) 38 | else 39 | super 40 | end 41 | end 42 | 43 | # Caches the json constructed within the block passed. Has the same signature as the `cache` helper 44 | # method in `ActionView::Helpers::CacheHelper` and so can be used in the same way. 45 | # 46 | # Example: 47 | # 48 | # json.cache! ['v1', @person], expires_in: 10.minutes do 49 | # json.extract! @person, :name, :age 50 | # end 51 | def cache!(key=nil, options={}) 52 | if @context.controller.perform_caching 53 | value = ::Rails.cache.fetch(_cache_key(key, options), options) do 54 | _scope { yield self } 55 | end 56 | 57 | merge! value 58 | else 59 | yield 60 | end 61 | end 62 | 63 | # Conditionally catches the json depending in the condition given as first parameter. Has the same 64 | # signature as the `cache` helper method in `ActionView::Helpers::CacheHelper` and so can be used in 65 | # the same way. 66 | # 67 | # Example: 68 | # 69 | # json.cache_if! !admin?, @person, expires_in: 10.minutes do 70 | # json.extract! @person, :name, :age 71 | # end 72 | def cache_if!(condition, *args, &block) 73 | condition ? cache!(*args, &block) : yield 74 | end 75 | 76 | protected 77 | 78 | def _render_partial_with_options(options) 79 | options.reverse_merge! locals: {} 80 | options.reverse_merge! ::JbuilderTemplate.template_lookup_options 81 | as = options[:as] 82 | 83 | if as && options.key?(:collection) 84 | as = as.to_sym 85 | collection = options.delete(:collection) 86 | locals = options.delete(:locals) 87 | array! collection do |member| 88 | member_locals = locals.clone 89 | member_locals.merge! collection: collection 90 | member_locals.merge! as => member 91 | _render_partial options.merge(locals: member_locals) 92 | end 93 | else 94 | _render_partial options 95 | end 96 | end 97 | 98 | def _render_partial(options) 99 | options[:locals].merge! json: self 100 | @context.render options 101 | end 102 | 103 | def _cache_key(key, options) 104 | key = _fragment_name_with_digest(key, options) 105 | key = url_for(key).split('://', 2).last if ::Hash === key 106 | ::ActiveSupport::Cache.expand_cache_key(key, :jbuilder) 107 | end 108 | 109 | private 110 | 111 | def _fragment_name_with_digest(key, options) 112 | if @context.respond_to?(:cache_fragment_name) 113 | # Current compatibility, fragment_name_with_digest is private again and cache_fragment_name 114 | # should be used instead. 115 | @context.cache_fragment_name(key, options) 116 | elsif @context.respond_to?(:fragment_name_with_digest) 117 | # Backwards compatibility for period of time when fragment_name_with_digest was made public. 118 | @context.fragment_name_with_digest(key) 119 | else 120 | key 121 | end 122 | end 123 | 124 | def _mapable_arguments?(value, *args) 125 | return true if super 126 | options = args.last 127 | ::Hash === options && options.key?(:as) 128 | end 129 | end 130 | 131 | class JbuilderHandler 132 | cattr_accessor :default_format 133 | self.default_format = Mime::JSON 134 | 135 | def self.call(template) 136 | # this juggling is required to keep line numbers right in the error 137 | %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{template.source} 138 | json.target! unless (__already_defined && __already_defined != "method")} 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 2.2.6 4 | ----- 5 | * [Make sure dependency tracker loads after template handler](https://github.com/rails/jbuilder/commit/3ba404b1207b557e14771c90b8832bc01ae67a42) 6 | 7 | 2.2.5 8 | ----- 9 | * [Refactor merge block behavior to raise error for unexpected values](https://github.com/rails/jbuilder/commit/4503162fb26f53f613fc83ac081fd244748b6fe9) 10 | 11 | 2.2.4 12 | ----- 13 | * [Typecast locals hash key during collection render](https://github.com/rails/jbuilder/commit/a6b0c8651a08e01cb53eee38e211c65423f275f7) 14 | 15 | 2.2.3 16 | ----- 17 | * [Move template handler registration into railtie](https://github.com/rails/jbuilder/commit/c8acc5cea6da2a79b7b345adc301cb5ff2517647) 18 | * [Do not capture the block where it is possible](https://github.com/rails/jbuilder/commit/973b382c3924cb59fc0e4e25266b18e74d41d646) 19 | 20 | 2.2.2 21 | ----- 22 | * [Fix `Jbuilder#merge!` inside block](https://github.com/rails/jbuilder/commit/a7b328552eb0d36315f75bff813bea7eecf8c1d7) 23 | 24 | 2.2.1 25 | ----- 26 | * [Fix empty block handling](https://github.com/rails/jbuilder/commit/972a11141403269e9b17b45b0c95f8a9788245ee) 27 | 28 | 2.2.0 29 | ----- 30 | * [Allow to skip `array!` iterations by calling `next`](https://github.com/rails/jbuilder/commit/81a63308fb9d5002519dd871f829ccc58067251a) 31 | 32 | 2.1.2 33 | ----- 34 | * [Cast array-like objects to array before merging](https://github.com/rails/jbuilder/commit/7b8c8a1cb09b7f3dd26e5643ebbd6b2ec67185db) 35 | 36 | 2.1.1 37 | ----- 38 | * [Remove unused file](https://github.com/rails/jbuilder/commit/e49e1047976fac93b8242ab212c7b1a463b70809) 39 | 40 | 2.1.0 41 | ----- 42 | * [Blocks and their extract! shortcuts are additive by default](https://github.com/rails/jbuilder/commit/a49390736c5f6e2d7a31111df6531bc28dba9fb1) 43 | 44 | 2.0.8 45 | ----- 46 | * [Eliminate circular dependencies](https://github.com/rails/jbuilder/commit/0879484dc74e7be93b695f66e3708ba48cdb1be3) 47 | * [Support cache key generation for complex objects](https://github.com/rails/jbuilder/commit/ca9622cca30c1112dd4408fcb2e658849abe1dd5) 48 | * [Remove JbuilderProxy class](https://github.com/rails/jbuilder/commit/5877482fc7da3224e42d4f72a1386f7a3a08173b) 49 | * [Move KeyFormatter into a separate file](https://github.com/rails/jbuilder/commit/13fee8464ff53ce853030114283c03c135c052b6) 50 | * [Move NullError into a separate file](https://github.com/rails/jbuilder/commit/13fee8464ff53ce853030114283c03c135c052b6) 51 | 52 | 2.0.7 53 | ----- 54 | * [Add destroy notice to scaffold generator](https://github.com/rails/jbuilder/commit/8448e86f8cdfa0f517bd59576947875775a1d43c) 55 | 56 | 2.0.6 57 | ----- 58 | * [Use render short form in controller generator](https://github.com/rails/jbuilder/commit/acf37320a7cea7fcc70c791bc94bd5f46b8349ff) 59 | 60 | 2.0.5 61 | ----- 62 | * [Fix edgecase when json is defined as a method](https://github.com/rails/jbuilder/commit/ca711a0c0a5760e26258ce2d93c14bef8fff0ead) 63 | 64 | 2.0.4 65 | ----- 66 | * [Add cache_if! to conditionally cache JSON fragments](https://github.com/rails/jbuilder/commit/14a5afd8a2c939a6fd710d355a194c114db96eb2) 67 | 68 | 2.0.3 69 | ----- 70 | * [Pass options when calling cache_fragment_name](https://github.com/rails/jbuilder/commit/07c2cc7486fe9ef423d7bc821b83f6d485f330e0) 71 | 72 | 2.0.2 73 | ----- 74 | * [Fix Dependency Tracking fail to detect single-quoted partial correctly](https://github.com/rails/jbuilder/commit/448679a6d3098eb34d137f782a05f1767711991a) 75 | * [Prevent Dependency Tracker constants leaking into global namespace](https://github.com/rails/jbuilder/commit/3544b288b63f504f46fa8aafd1d17ee198d77536) 76 | 77 | 2.0.1 78 | ----- 79 | * [Dependency tracking support for Rails 3 with cache_digest gem](https://github.com/rails/jbuilder/commit/6b471d7a38118e8f7645abec21955ef793401daf) 80 | 81 | 2.0.0 82 | ----- 83 | * [Respond to PUT/PATCH API request with :ok](https://github.com/rails/jbuilder/commit/9dbce9c12181e89f8f472ac23c764ffe8438040a) 84 | * [Remove Ruby 1.8 support](https://github.com/rails/jbuilder/commit/d53fff42d91f33d662eafc2561c4236687ecf6c9) 85 | * [Remove deprecated two argument block call](https://github.com/rails/jbuilder/commit/07a35ee7e79ae4b06dba9dbff5c4e07c1e627218) 86 | * [Make Jbuilder object initialize with single hash](https://github.com/rails/jbuilder/commit/38bf551db0189327aaa90b9be010c0d1b792c007) 87 | * [Track template dependencies](https://github.com/rails/jbuilder/commit/8e73cea39f60da1384afd687cc8e5e399630d8cc) 88 | * [Expose merge! method](https://github.com/rails/jbuilder/commit/0e2eb47f6f3c01add06a1a59b37cdda8baf24f29) 89 | 90 | 1.5.3 91 | ----- 92 | * [Generators add `:id` column by default](https://github.com/rails/jbuilder/commit/0b52b86773e48ac2ce35d4155c7b70ad8b3e8937) 93 | 94 | 1.5.2 95 | ----- 96 | * [Nil-collection should be treated as empty array](https://github.com/rails/jbuilder/commit/2f700bb00ab663c6b7fcb28d2967aeb989bd43c7) 97 | 98 | 1.5.1 99 | ----- 100 | * [Expose template lookup options](https://github.com/rails/jbuilder/commit/404c18dee1af96ac6d8052a04062629ef1db2945) 101 | 102 | 1.5.0 103 | ----- 104 | * [Do not perform any caching when `controller.perform_caching` is false](https://github.com/rails/jbuilder/commit/94633facde1ac43580f8cd5e13ae9cc83e1da8f4) 105 | * [Add partial collection rendering](https://github.com/rails/jbuilder/commit/e8c10fc885e41b18178aaf4dcbc176961c928d76) 106 | * [Deprecate extract! calling private methods](https://github.com/rails/jbuilder/commit/b9e19536c2105d7f2e813006bbcb8ca5730d28a3) 107 | * [Add array of partials rendering](https://github.com/rails/jbuilder/commit/7d7311071720548047f98f14ad013c560b8d9c3a) 108 | 109 | 1.4.2 110 | ----- 111 | * [Require MIME dependency explicitly](https://github.com/rails/jbuilder/commit/b1ed5ac4f08b056f8839b4b19b43562e81e02a59) 112 | 113 | 1.4.1 114 | ----- 115 | * [Removed deprecated positioned arguments initializer support](https://github.com/rails/jbuilder/commit/6e03e0452073eeda77e6dfe66aa31e5ec67a3531) 116 | * [Deprecate two-arguments block calling](https://github.com/rails/jbuilder/commit/2b10bb058bb12bc782cbcc16f6ec67b489e5ed43) 117 | 118 | 1.4.0 119 | ----- 120 | * [Add quick collection attribute extraction](https://github.com/rails/jbuilder/commit/c2b966cf653ea4264fbb008b8cc6ce5359ebe40a) 121 | * [Block has priority over attributes extraction](https://github.com/rails/jbuilder/commit/77c24766362c02769d81dac000b1879a9e4d4a00) 122 | * [Meaningfull error messages when adding properties to null](https://github.com/rails/jbuilder/commit/e26764602e34b3772e57e730763d512e59489e3b) 123 | * [Do not enforce template format, enforce handlers instead](https://github.com/rails/jbuilder/commit/72576755224b15da45e50cbea877679800ab1398) 124 | 125 | 1.3.0 126 | ----- 127 | * [Add nil! method for nil JSON](https://github.com/rails/jbuilder/commit/822a906f68664f61a1209336bb681077692c8475) 128 | 129 | 1.2.1 130 | ----- 131 | * [Added explicit dependency for MultiJson](https://github.com/rails/jbuilder/commit/4d58eacb6cd613679fb243484ff73a79bbbff2d2 132 | 133 | 1.2.0 134 | ----- 135 | * Multiple documentation improvements and internal refactoring 136 | * [Fixes fragment caching to work with latest digests](https://github.com/rails/jbuilder/commit/da937d6b8732124074c612abb7ff38868d1d96c0) 137 | 138 | 1.0.2 139 | ----- 140 | * [Support non-Enumerable collections](https://github.com/rails/jbuilder/commit/4c20c59bf8131a1e419bb4ebf84f2b6bdcb6b0cf) 141 | * [Ensure that the default URL is in json format](https://github.com/rails/jbuilder/commit/0b46782fb7b8c34a3c96afa801fe27a5a97118a4) 142 | 143 | 1.0.0 144 | ----- 145 | * Adopt Semantic Versioning 146 | * Add rails generators 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jbuilder 2 | 3 | [![Build Status](https://api.travis-ci.org/rails/jbuilder.svg)][travis] 4 | [![Gem Version](http://img.shields.io/gem/v/jbuilder.svg)][gem] 5 | [![Code Climate](http://img.shields.io/codeclimate/github/rails/jbuilder.svg)][codeclimate] 6 | [![Dependencies Status](http://img.shields.io/gemnasium/rails/jbuilder.svg)][gemnasium] 7 | 8 | [travis]: https://travis-ci.org/rails/jbuilder 9 | [gem]: https://rubygems.org/gems/jbuilder 10 | [codeclimate]: https://codeclimate.com/github/rails/jbuilder 11 | [gemnasium]: https://gemnasium.com/rails/jbuilder 12 | 13 | Jbuilder gives you a simple DSL for declaring JSON structures that beats 14 | massaging giant hash structures. This is particularly helpful when the 15 | generation process is fraught with conditionals and loops. Here's a simple 16 | example: 17 | 18 | ``` ruby 19 | # app/views/message/show.json.jbuilder 20 | 21 | json.content format_content(@message.content) 22 | json.(@message, :created_at, :updated_at) 23 | 24 | json.author do 25 | json.name @message.creator.name.familiar 26 | json.email_address @message.creator.email_address_with_name 27 | json.url url_for(@message.creator, format: :json) 28 | end 29 | 30 | if current_user.admin? 31 | json.visitors calculate_visitors(@message) 32 | end 33 | 34 | json.comments @message.comments, :content, :created_at 35 | 36 | json.attachments @message.attachments do |attachment| 37 | json.filename attachment.filename 38 | json.url url_for(attachment) 39 | end 40 | ``` 41 | 42 | This will build the following structure: 43 | 44 | ``` javascript 45 | { 46 | "content": "

This is serious monkey business

", 47 | "created_at": "2011-10-29T20:45:28-05:00", 48 | "updated_at": "2011-10-29T20:45:28-05:00", 49 | 50 | "author": { 51 | "name": "David H.", 52 | "email_address": "'David Heinemeier Hansson' ", 53 | "url": "http://example.com/users/1-david.json" 54 | }, 55 | 56 | "visitors": 15, 57 | 58 | "comments": [ 59 | { "content": "Hello everyone!", "created_at": "2011-10-29T20:45:28-05:00" }, 60 | { "content": "To you my good sir!", "created_at": "2011-10-29T20:47:28-05:00" } 61 | ], 62 | 63 | "attachments": [ 64 | { "filename": "forecast.xls", "url": "http://example.com/downloads/forecast.xls" }, 65 | { "filename": "presentation.pdf", "url": "http://example.com/downloads/presentation.pdf" } 66 | ] 67 | } 68 | ``` 69 | 70 | To define attribute and structure names dynamically, use the `set!` method: 71 | 72 | ``` ruby 73 | json.set! :author do 74 | json.set! :name, 'David' 75 | end 76 | 77 | # => "author": { "name": "David" } 78 | ``` 79 | 80 | Top level arrays can be handled directly. Useful for index and other collection actions. 81 | 82 | ``` ruby 83 | # @comments = @post.comments 84 | 85 | json.array! @comments do |comment| 86 | next if comment.marked_as_spam_by?(current_user) 87 | 88 | json.body comment.body 89 | json.author do 90 | json.first_name comment.author.first_name 91 | json.last_name comment.author.last_name 92 | end 93 | end 94 | 95 | # => [ { "body": "great post...", "author": { "first_name": "Joe", "last_name": "Bloe" }} ] 96 | ``` 97 | 98 | You can also extract attributes from array directly. 99 | 100 | ``` ruby 101 | # @people = People.all 102 | 103 | json.array! @people, :id, :name 104 | 105 | # => [ { "id": 1, "name": "David" }, { "id": 2, "name": "Jamie" } ] 106 | ``` 107 | 108 | Jbuilder objects can be directly nested inside each other. Useful for composing objects. 109 | 110 | ``` ruby 111 | class Person 112 | # ... Class Definition ... # 113 | def to_builder 114 | Jbuilder.new do |person| 115 | person.(self, :name, :age) 116 | end 117 | end 118 | end 119 | 120 | class Company 121 | # ... Class Definition ... # 122 | def to_builder 123 | Jbuilder.new do |company| 124 | company.name name 125 | company.president president.to_builder 126 | end 127 | end 128 | end 129 | 130 | company = Company.new('Doodle Corp', Person.new('John Stobs', 58)) 131 | company.to_builder.target! 132 | 133 | # => {"name":"Doodle Corp","president":{"name":"John Stobs","age":58}} 134 | ``` 135 | 136 | You can either use Jbuilder stand-alone or directly as an ActionView template 137 | language. When required in Rails, you can create views ala show.json.jbuilder 138 | (the json is already yielded): 139 | 140 | ``` ruby 141 | # Any helpers available to views are available to the builder 142 | json.content format_content(@message.content) 143 | json.(@message, :created_at, :updated_at) 144 | 145 | json.author do 146 | json.name @message.creator.name.familiar 147 | json.email_address @message.creator.email_address_with_name 148 | json.url url_for(@message.creator, format: :json) 149 | end 150 | 151 | if current_user.admin? 152 | json.visitors calculate_visitors(@message) 153 | end 154 | ``` 155 | 156 | 157 | You can use partials as well. The following will render the file 158 | `views/comments/_comments.json.jbuilder`, and set a local variable 159 | `comments` with all this message's comments, which you can use inside 160 | the partial. 161 | 162 | ```ruby 163 | json.partial! 'comments/comments', comments: @message.comments 164 | ``` 165 | 166 | It's also possible to render collections of partials: 167 | 168 | ```ruby 169 | json.array! @posts, partial: 'posts/post', as: :post 170 | 171 | # or 172 | 173 | json.partial! 'posts/post', collection: @posts, as: :post 174 | 175 | # or 176 | 177 | json.partial! partial: 'posts/post', collection: @posts, as: :post 178 | 179 | # or 180 | 181 | json.comments @post.comments, partial: 'comment/comment', as: :comment 182 | ``` 183 | 184 | You can explicitly make Jbuilder object return null if you want: 185 | 186 | ``` ruby 187 | json.extract! @post, :id, :title, :content, :published_at 188 | json.author do 189 | if @post.anonymous? 190 | json.null! # or json.nil! 191 | else 192 | json.first_name @post.author_first_name 193 | json.last_name @post.author_last_name 194 | end 195 | end 196 | ``` 197 | 198 | Fragment caching is supported, it uses `Rails.cache` and works like caching in 199 | HTML templates: 200 | 201 | ```ruby 202 | json.cache! ['v1', @person], expires_in: 10.minutes do 203 | json.extract! @person, :name, :age 204 | end 205 | ``` 206 | 207 | You can also conditionally cache a block by using `cache_if!` like this: 208 | 209 | ```ruby 210 | json.cache_if! !admin?, ['v1', @person], expires_in: 10.minutes do 211 | json.extract! @person, :name, :age 212 | end 213 | ``` 214 | 215 | If you are rendering fragments for a collection of objects, have a look at 216 | `jbuilder_cache_multi` gem. It uses fetch_multi (>= Rails 4.1) to fetch 217 | multiple keys at once. 218 | 219 | Keys can be auto formatted using `key_format!`, this can be used to convert 220 | keynames from the standard ruby_format to camelCase: 221 | 222 | ``` ruby 223 | json.key_format! camelize: :lower 224 | json.first_name 'David' 225 | 226 | # => { "firstName": "David" } 227 | ``` 228 | 229 | You can set this globally with the class method `key_format` (from inside your 230 | environment.rb for example): 231 | 232 | ``` ruby 233 | Jbuilder.key_format camelize: :lower 234 | ``` 235 | 236 | Faster JSON backends 237 | -------------------- 238 | 239 | Jbuilder uses MultiJson, which by default will use the JSON gem. That gem is 240 | currently tangled with ActiveSupport's all-Ruby `#to_json` implementation, 241 | which is slow (fixed in Rails >= 4.1). For faster Jbuilder rendering, you can 242 | specify something like the Yajl JSON generator instead. You'll need to include 243 | the `yajl-ruby` gem in your Gemfile and then set the following configuration 244 | for MultiJson: 245 | 246 | ``` ruby 247 | require 'multi_json' 248 | MultiJson.use :yajl 249 | ``` 250 | -------------------------------------------------------------------------------- /lib/jbuilder.rb: -------------------------------------------------------------------------------- 1 | require 'jbuilder/jbuilder' 2 | require 'jbuilder/key_formatter' 3 | require 'jbuilder/errors' 4 | require 'multi_json' 5 | 6 | class Jbuilder 7 | @@key_formatter = KeyFormatter.new 8 | @@ignore_nil = false 9 | 10 | def initialize(options = {}) 11 | @attributes = {} 12 | 13 | @key_formatter = options.fetch(:key_formatter){ @@key_formatter.clone } 14 | @ignore_nil = options.fetch(:ignore_nil, @@ignore_nil) 15 | 16 | yield self if ::Kernel.block_given? 17 | end 18 | 19 | # Yields a builder and automatically turns the result into a JSON string 20 | def self.encode(*args, &block) 21 | new(*args, &block).target! 22 | end 23 | 24 | BLANK = ::Object.new 25 | 26 | def set!(key, value = BLANK, *args, &block) 27 | result = if block 28 | if !_blank?(value) 29 | # json.comments @post.comments { |comment| ... } 30 | # { "comments": [ { ... }, { ... } ] } 31 | _scope{ array! value, &block } 32 | else 33 | # json.comments { ... } 34 | # { "comments": ... } 35 | _merge_block(key){ yield self } 36 | end 37 | elsif args.empty? 38 | if ::Jbuilder === value 39 | # json.age 32 40 | # json.person another_jbuilder 41 | # { "age": 32, "person": { ... } 42 | value.attributes! 43 | else 44 | # json.age 32 45 | # { "age": 32 } 46 | value 47 | end 48 | elsif _mapable_arguments?(value, *args) 49 | # json.comments @post.comments, :content, :created_at 50 | # { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] } 51 | _scope{ array! value, *args } 52 | else 53 | # json.author @post.creator, :name, :email_address 54 | # { "author": { "name": "David", "email_address": "david@loudthinking.com" } } 55 | _merge_block(key){ extract! value, *args } 56 | end 57 | 58 | _set_value key, result 59 | end 60 | 61 | alias_method :method_missing, :set! 62 | private :method_missing 63 | 64 | # Specifies formatting to be applied to the key. Passing in a name of a function 65 | # will cause that function to be called on the key. So :upcase will upper case 66 | # the key. You can also pass in lambdas for more complex transformations. 67 | # 68 | # Example: 69 | # 70 | # json.key_format! :upcase 71 | # json.author do 72 | # json.name "David" 73 | # json.age 32 74 | # end 75 | # 76 | # { "AUTHOR": { "NAME": "David", "AGE": 32 } } 77 | # 78 | # You can pass parameters to the method using a hash pair. 79 | # 80 | # json.key_format! camelize: :lower 81 | # json.first_name "David" 82 | # 83 | # { "firstName": "David" } 84 | # 85 | # Lambdas can also be used. 86 | # 87 | # json.key_format! ->(key){ "_" + key } 88 | # json.first_name "David" 89 | # 90 | # { "_first_name": "David" } 91 | # 92 | def key_format!(*args) 93 | @key_formatter = KeyFormatter.new(*args) 94 | end 95 | 96 | # Same as the instance method key_format! except sets the default. 97 | def self.key_format(*args) 98 | @@key_formatter = KeyFormatter.new(*args) 99 | end 100 | 101 | # If you want to skip adding nil values to your JSON hash. This is useful 102 | # for JSON clients that don't deal well with nil values, and would prefer 103 | # not to receive keys which have null values. 104 | # 105 | # Example: 106 | # json.ignore_nil! false 107 | # json.id User.new.id 108 | # 109 | # { "id": null } 110 | # 111 | # json.ignore_nil! 112 | # json.id User.new.id 113 | # 114 | # {} 115 | # 116 | def ignore_nil!(value = true) 117 | @ignore_nil = value 118 | end 119 | 120 | # Same as instance method ignore_nil! except sets the default. 121 | def self.ignore_nil(value = true) 122 | @@ignore_nil = value 123 | end 124 | 125 | # Turns the current element into an array and yields a builder to add a hash. 126 | # 127 | # Example: 128 | # 129 | # json.comments do 130 | # json.child! { json.content "hello" } 131 | # json.child! { json.content "world" } 132 | # end 133 | # 134 | # { "comments": [ { "content": "hello" }, { "content": "world" } ]} 135 | # 136 | # More commonly, you'd use the combined iterator, though: 137 | # 138 | # json.comments(@post.comments) do |comment| 139 | # json.content comment.formatted_content 140 | # end 141 | def child! 142 | @attributes = [] unless ::Array === @attributes 143 | @attributes << _scope{ yield self } 144 | end 145 | 146 | # Turns the current element into an array and iterates over the passed collection, adding each iteration as 147 | # an element of the resulting array. 148 | # 149 | # Example: 150 | # 151 | # json.array!(@people) do |person| 152 | # json.name person.name 153 | # json.age calculate_age(person.birthday) 154 | # end 155 | # 156 | # [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ] 157 | # 158 | # If you are using Ruby 1.9+, you can use the call syntax instead of an explicit extract! call: 159 | # 160 | # json.(@people) { |person| ... } 161 | # 162 | # It's generally only needed to use this method for top-level arrays. If you have named arrays, you can do: 163 | # 164 | # json.people(@people) do |person| 165 | # json.name person.name 166 | # json.age calculate_age(person.birthday) 167 | # end 168 | # 169 | # { "people": [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ] } 170 | # 171 | # If you omit the block then you can set the top level array directly: 172 | # 173 | # json.array! [1, 2, 3] 174 | # 175 | # [1,2,3] 176 | def array!(collection = [], *attributes, &block) 177 | array = if collection.nil? 178 | [] 179 | elsif block 180 | _map_collection(collection, &block) 181 | elsif attributes.any? 182 | _map_collection(collection) { |element| extract! element, *attributes } 183 | else 184 | collection.to_a 185 | end 186 | 187 | merge! array 188 | end 189 | 190 | # Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON. 191 | # 192 | # Example: 193 | # 194 | # @person = Struct.new(:name, :age).new('David', 32) 195 | # 196 | # or you can utilize a Hash 197 | # 198 | # @person = { name: 'David', age: 32 } 199 | # 200 | # json.extract! @person, :name, :age 201 | # 202 | # { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } 203 | # 204 | # You can also use the call syntax instead of an explicit extract! call: 205 | # 206 | # json.(@person, :name, :age) 207 | def extract!(object, *attributes) 208 | if ::Hash === object 209 | _extract_hash_values(object, *attributes) 210 | else 211 | _extract_method_values(object, *attributes) 212 | end 213 | end 214 | 215 | def call(object, *attributes, &block) 216 | if block 217 | array! object, &block 218 | else 219 | extract! object, *attributes 220 | end 221 | end 222 | 223 | # Returns the nil JSON. 224 | def nil! 225 | @attributes = nil 226 | end 227 | 228 | alias_method :null!, :nil! 229 | 230 | # Returns the attributes of the current builder. 231 | def attributes! 232 | @attributes 233 | end 234 | 235 | # Merges hash or array into current builder. 236 | def merge!(hash_or_array) 237 | @attributes = _merge_values(@attributes, hash_or_array) 238 | end 239 | 240 | # Encodes the current builder as JSON. 241 | def target! 242 | ::MultiJson.dump(@attributes) 243 | end 244 | 245 | private 246 | 247 | def _extract_hash_values(object, *attributes) 248 | attributes.each{ |key| _set_value key, object.fetch(key) } 249 | end 250 | 251 | def _extract_method_values(object, *attributes) 252 | attributes.each{ |key| _set_value key, object.public_send(key) } 253 | end 254 | 255 | def _merge_block(key) 256 | current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK) 257 | raise NullError.build(key) if current_value.nil? 258 | new_value = _scope{ yield self } 259 | _merge_values(current_value, new_value) 260 | end 261 | 262 | def _merge_values(current_value, updates) 263 | if _blank?(updates) 264 | current_value 265 | elsif _blank?(current_value) || updates.nil? 266 | updates 267 | elsif ::Array === updates 268 | ::Array === current_value ? current_value + updates : updates 269 | elsif ::Hash === current_value 270 | current_value.merge(updates) 271 | else 272 | raise "Can't merge #{updates.inspect} with #{current_value.inspect}" 273 | end 274 | end 275 | 276 | def _write(key, value) 277 | @attributes = {} if _blank? 278 | @attributes[_key(key)] = value 279 | end 280 | 281 | def _key(key) 282 | @key_formatter.format(key) 283 | end 284 | 285 | def _set_value(key, value) 286 | raise NullError.build(key) if @attributes.nil? 287 | return if @ignore_nil && value.nil? 288 | return if _blank?(value) 289 | _write key, value 290 | end 291 | 292 | def _map_collection(collection) 293 | collection.map do |element| 294 | _scope{ yield element } 295 | end - [BLANK] 296 | end 297 | 298 | def _scope 299 | parent_attributes, parent_formatter = @attributes, @key_formatter 300 | @attributes = BLANK 301 | yield 302 | @attributes 303 | ensure 304 | @attributes, @key_formatter = parent_attributes, parent_formatter 305 | end 306 | 307 | def _mapable_arguments?(value, *args) 308 | value.respond_to?(:map) 309 | end 310 | 311 | def _blank?(value=@attributes) 312 | BLANK == value 313 | end 314 | end 315 | 316 | require 'jbuilder/railtie' if defined?(Rails) 317 | -------------------------------------------------------------------------------- /test/jbuilder_template_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'mocha/setup' 3 | require 'action_view' 4 | require 'action_view/testing/resolvers' 5 | require 'active_support/cache' 6 | require 'jbuilder/jbuilder_template' 7 | 8 | BLOG_POST_PARTIAL = <<-JBUILDER 9 | json.extract! blog_post, :id, :body 10 | json.author do 11 | name = blog_post.author_name.split(nil, 2) 12 | json.first_name name[0] 13 | json.last_name name[1] 14 | end 15 | JBUILDER 16 | 17 | COLLECTION_PARTIAL = <<-JBUILDER 18 | json.extract! collection, :id, :name 19 | JBUILDER 20 | 21 | BlogPost = Struct.new(:id, :body, :author_name) 22 | Collection = Struct.new(:id, :name) 23 | blog_authors = [ 'David Heinemeier Hansson', 'Pavel Pravosud' ].cycle 24 | BLOG_POST_COLLECTION = 10.times.map{ |i| BlogPost.new(i+1, "post body #{i+1}", blog_authors.next) } 25 | COLLECTION_COLLECTION = 5.times.map{ |i| Collection.new(i+1, "collection #{i+1}") } 26 | 27 | ActionView::Template.register_template_handler :jbuilder, JbuilderHandler 28 | 29 | module Rails 30 | def self.cache 31 | @cache ||= ActiveSupport::Cache::MemoryStore.new 32 | end 33 | end 34 | 35 | class JbuilderTemplateTest < ActionView::TestCase 36 | setup do 37 | @context = self 38 | Rails.cache.clear 39 | end 40 | 41 | def partials 42 | { 43 | '_partial.json.jbuilder' => 'json.content "hello"', 44 | '_blog_post.json.jbuilder' => BLOG_POST_PARTIAL, 45 | '_collection.json.jbuilder' => COLLECTION_PARTIAL 46 | } 47 | end 48 | 49 | def render_jbuilder(source) 50 | @rendered = [] 51 | lookup_context.view_paths = [ActionView::FixtureResolver.new(partials.merge('test.json.jbuilder' => source))] 52 | ActionView::Template.new(source, 'test', JbuilderHandler, :virtual_path => 'test').render(self, {}).strip 53 | end 54 | 55 | def undef_context_methods(*names) 56 | self.class_eval do 57 | names.each do |name| 58 | undef_method name.to_sym if method_defined?(name.to_sym) 59 | end 60 | end 61 | end 62 | 63 | def assert_collection_rendered(json, context = nil) 64 | result = MultiJson.load(json) 65 | result = result.fetch(context) if context 66 | 67 | assert_equal 10, result.length 68 | assert_equal Array, result.class 69 | assert_equal 'post body 5', result[4]['body'] 70 | assert_equal 'Heinemeier Hansson', result[2]['author']['last_name'] 71 | assert_equal 'Pavel', result[5]['author']['first_name'] 72 | end 73 | 74 | test 'rendering' do 75 | json = render_jbuilder <<-JBUILDER 76 | json.content 'hello' 77 | JBUILDER 78 | 79 | assert_equal 'hello', MultiJson.load(json)['content'] 80 | end 81 | 82 | test 'key_format! with parameter' do 83 | json = render_jbuilder <<-JBUILDER 84 | json.key_format! :camelize => [:lower] 85 | json.camel_style 'for JS' 86 | JBUILDER 87 | 88 | assert_equal ['camelStyle'], MultiJson.load(json).keys 89 | end 90 | 91 | test 'key_format! propagates to child elements' do 92 | json = render_jbuilder <<-JBUILDER 93 | json.key_format! :upcase 94 | json.level1 'one' 95 | json.level2 do 96 | json.value 'two' 97 | end 98 | JBUILDER 99 | 100 | result = MultiJson.load(json) 101 | assert_equal 'one', result['LEVEL1'] 102 | assert_equal 'two', result['LEVEL2']['VALUE'] 103 | end 104 | 105 | test 'partial! renders partial' do 106 | json = render_jbuilder <<-JBUILDER 107 | json.partial! 'partial' 108 | JBUILDER 109 | 110 | assert_equal 'hello', MultiJson.load(json)['content'] 111 | end 112 | 113 | test 'partial! renders collections' do 114 | json = render_jbuilder <<-JBUILDER 115 | json.partial! 'blog_post', :collection => BLOG_POST_COLLECTION, :as => :blog_post 116 | JBUILDER 117 | 118 | assert_collection_rendered json 119 | end 120 | 121 | test 'partial! renders collections when as argument is a string' do 122 | json = render_jbuilder <<-JBUILDER 123 | json.partial! 'blog_post', collection: BLOG_POST_COLLECTION, as: "blog_post" 124 | JBUILDER 125 | 126 | assert_collection_rendered json 127 | end 128 | 129 | test 'partial! renders collections as collections' do 130 | json = render_jbuilder <<-JBUILDER 131 | json.partial! 'collection', collection: COLLECTION_COLLECTION, as: :collection 132 | JBUILDER 133 | 134 | assert_equal 5, MultiJson.load(json).length 135 | end 136 | 137 | test 'partial! renders as empty array for nil-collection' do 138 | json = render_jbuilder <<-JBUILDER 139 | json.partial! 'blog_post', :collection => nil, :as => :blog_post 140 | JBUILDER 141 | 142 | assert_equal '[]', json 143 | end 144 | 145 | test 'partial! renders collection (alt. syntax)' do 146 | json = render_jbuilder <<-JBUILDER 147 | json.partial! :partial => 'blog_post', :collection => BLOG_POST_COLLECTION, :as => :blog_post 148 | JBUILDER 149 | 150 | assert_collection_rendered json 151 | end 152 | 153 | test 'partial! renders as empty array for nil-collection (alt. syntax)' do 154 | json = render_jbuilder <<-JBUILDER 155 | json.partial! :partial => 'blog_post', :collection => nil, :as => :blog_post 156 | JBUILDER 157 | 158 | assert_equal '[]', json 159 | end 160 | 161 | test 'render array of partials' do 162 | json = render_jbuilder <<-JBUILDER 163 | json.array! BLOG_POST_COLLECTION, :partial => 'blog_post', :as => :blog_post 164 | JBUILDER 165 | 166 | assert_collection_rendered json 167 | end 168 | 169 | test 'render array of partials as empty array with nil-collection' do 170 | json = render_jbuilder <<-JBUILDER 171 | json.array! nil, :partial => 'blog_post', :as => :blog_post 172 | JBUILDER 173 | 174 | assert_equal '[]', json 175 | end 176 | 177 | test 'render array if partials as a value' do 178 | json = render_jbuilder <<-JBUILDER 179 | json.posts BLOG_POST_COLLECTION, :partial => 'blog_post', :as => :blog_post 180 | JBUILDER 181 | 182 | assert_collection_rendered json, 'posts' 183 | end 184 | 185 | test 'render as empty array if partials as a nil value' do 186 | json = render_jbuilder <<-JBUILDER 187 | json.posts nil, :partial => 'blog_post', :as => :blog_post 188 | JBUILDER 189 | 190 | assert_equal '{"posts":[]}', json 191 | end 192 | 193 | test 'fragment caching a JSON object' do 194 | undef_context_methods :fragment_name_with_digest, :cache_fragment_name 195 | 196 | render_jbuilder <<-JBUILDER 197 | json.cache! 'cachekey' do 198 | json.name 'Cache' 199 | end 200 | JBUILDER 201 | 202 | json = render_jbuilder <<-JBUILDER 203 | json.cache! 'cachekey' do 204 | json.name 'Miss' 205 | end 206 | JBUILDER 207 | 208 | parsed = MultiJson.load(json) 209 | assert_equal 'Cache', parsed['name'] 210 | end 211 | 212 | test 'conditionally fragment caching a JSON object' do 213 | undef_context_methods :fragment_name_with_digest, :cache_fragment_name 214 | 215 | render_jbuilder <<-JBUILDER 216 | json.cache_if! true, 'cachekey' do 217 | json.test1 'Cache' 218 | end 219 | json.cache_if! false, 'cachekey' do 220 | json.test2 'Cache' 221 | end 222 | JBUILDER 223 | 224 | json = render_jbuilder <<-JBUILDER 225 | json.cache_if! true, 'cachekey' do 226 | json.test1 'Miss' 227 | end 228 | json.cache_if! false, 'cachekey' do 229 | json.test2 'Miss' 230 | end 231 | JBUILDER 232 | 233 | parsed = MultiJson.load(json) 234 | assert_equal 'Cache', parsed['test1'] 235 | assert_equal 'Miss', parsed['test2'] 236 | end 237 | 238 | test 'fragment caching deserializes an array' do 239 | undef_context_methods :fragment_name_with_digest, :cache_fragment_name 240 | 241 | render_jbuilder <<-JBUILDER 242 | json.cache! 'cachekey' do 243 | json.array! %w[a b c] 244 | end 245 | JBUILDER 246 | 247 | json = render_jbuilder <<-JBUILDER 248 | json.cache! 'cachekey' do 249 | json.array! %w[1 2 3] 250 | end 251 | JBUILDER 252 | 253 | parsed = MultiJson.load(json) 254 | assert_equal %w[a b c], parsed 255 | end 256 | 257 | test 'fragment caching works with previous version of cache digests' do 258 | undef_context_methods :cache_fragment_name 259 | 260 | @context.expects :fragment_name_with_digest 261 | 262 | render_jbuilder <<-JBUILDER 263 | json.cache! 'cachekey' do 264 | json.name 'Cache' 265 | end 266 | JBUILDER 267 | end 268 | 269 | test 'fragment caching works with current cache digests' do 270 | undef_context_methods :fragment_name_with_digest 271 | 272 | @context.expects :cache_fragment_name 273 | ActiveSupport::Cache.expects :expand_cache_key 274 | 275 | render_jbuilder <<-JBUILDER 276 | json.cache! 'cachekey' do 277 | json.name 'Cache' 278 | end 279 | JBUILDER 280 | end 281 | 282 | test 'current cache digest option accepts options' do 283 | undef_context_methods :fragment_name_with_digest 284 | 285 | @context.expects(:cache_fragment_name).with('cachekey', skip_digest: true) 286 | ActiveSupport::Cache.expects :expand_cache_key 287 | 288 | render_jbuilder <<-JBUILDER 289 | json.cache! 'cachekey', skip_digest: true do 290 | json.name 'Cache' 291 | end 292 | JBUILDER 293 | end 294 | 295 | test 'does not perform caching when controller.perform_caching is false' do 296 | controller.perform_caching = false 297 | render_jbuilder <<-JBUILDER 298 | json.cache! 'cachekey' do 299 | json.name 'Cache' 300 | end 301 | JBUILDER 302 | 303 | assert_equal Rails.cache.inspect[/entries=(\d+)/, 1], '0' 304 | end 305 | 306 | end 307 | -------------------------------------------------------------------------------- /test/jbuilder_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_support/inflector' 3 | require 'jbuilder' 4 | 5 | def jbuild(*args, &block) 6 | Jbuilder.new(*args, &block).attributes! 7 | end 8 | 9 | Comment = Struct.new(:content, :id) 10 | 11 | class NonEnumerable 12 | def initialize(collection) 13 | @collection = collection 14 | end 15 | 16 | def map(&block) 17 | @collection.map(&block) 18 | end 19 | end 20 | 21 | class VeryBasicWrapper < BasicObject 22 | def initialize(thing) 23 | @thing = thing 24 | end 25 | 26 | def method_missing(name, *args, &block) 27 | @thing.send name, *args, &block 28 | end 29 | end 30 | 31 | # This is not Struct, because structs are Enumerable 32 | class Person 33 | attr_reader :name, :age 34 | 35 | def initialize(name, age) 36 | @name, @age = name, age 37 | end 38 | end 39 | 40 | class RelationMock 41 | include Enumerable 42 | 43 | def each(&block) 44 | [Person.new('Bob', 30), Person.new('Frank', 50)].each(&block) 45 | end 46 | 47 | def empty? 48 | false 49 | end 50 | end 51 | 52 | 53 | class JbuilderTest < ActiveSupport::TestCase 54 | test 'single key' do 55 | result = jbuild do |json| 56 | json.content 'hello' 57 | end 58 | 59 | assert_equal 'hello', result['content'] 60 | end 61 | 62 | test 'single key with false value' do 63 | result = jbuild do |json| 64 | json.content false 65 | end 66 | 67 | assert_equal false, result['content'] 68 | end 69 | 70 | test 'single key with nil value' do 71 | result = jbuild do |json| 72 | json.content nil 73 | end 74 | 75 | assert result.has_key?('content') 76 | assert_equal nil, result['content'] 77 | end 78 | 79 | test 'multiple keys' do 80 | result = jbuild do |json| 81 | json.title 'hello' 82 | json.content 'world' 83 | end 84 | 85 | assert_equal 'hello', result['title'] 86 | assert_equal 'world', result['content'] 87 | end 88 | 89 | test 'extracting from object' do 90 | person = Struct.new(:name, :age).new('David', 32) 91 | 92 | result = jbuild do |json| 93 | json.extract! person, :name, :age 94 | end 95 | 96 | assert_equal 'David', result['name'] 97 | assert_equal 32, result['age'] 98 | end 99 | 100 | test 'extracting from object using call style for 1.9' do 101 | person = Struct.new(:name, :age).new('David', 32) 102 | 103 | result = jbuild do |json| 104 | json.(person, :name, :age) 105 | end 106 | 107 | assert_equal 'David', result['name'] 108 | assert_equal 32, result['age'] 109 | end 110 | 111 | test 'extracting from hash' do 112 | person = {:name => 'Jim', :age => 34} 113 | 114 | result = jbuild do |json| 115 | json.extract! person, :name, :age 116 | end 117 | 118 | assert_equal 'Jim', result['name'] 119 | assert_equal 34, result['age'] 120 | end 121 | 122 | test 'nesting single child with block' do 123 | result = jbuild do |json| 124 | json.author do 125 | json.name 'David' 126 | json.age 32 127 | end 128 | end 129 | 130 | assert_equal 'David', result['author']['name'] 131 | assert_equal 32, result['author']['age'] 132 | end 133 | 134 | test 'empty block handling' do 135 | result = jbuild do |json| 136 | json.foo 'bar' 137 | json.author do 138 | end 139 | end 140 | 141 | assert_equal 'bar', result['foo'] 142 | assert !result.key?('author') 143 | end 144 | 145 | test 'blocks are additive' do 146 | result = jbuild do |json| 147 | json.author do 148 | json.name 'David' 149 | end 150 | 151 | json.author do 152 | json.age 32 153 | end 154 | end 155 | 156 | assert_equal 'David', result['author']['name'] 157 | assert_equal 32, result['author']['age'] 158 | end 159 | 160 | test 'support merge! method' do 161 | result = jbuild do |json| 162 | json.merge! 'foo' => 'bar' 163 | end 164 | 165 | assert_equal 'bar', result['foo'] 166 | end 167 | 168 | test 'support merge! method in a block' do 169 | result = jbuild do |json| 170 | json.author do 171 | json.merge! 'name' => 'Pavel' 172 | end 173 | end 174 | 175 | assert_equal 'Pavel', result['author']['name'] 176 | end 177 | 178 | test 'blocks are additive via extract syntax' do 179 | person = Person.new('Pavel', 27) 180 | 181 | result = jbuild do |json| 182 | json.author person, :age 183 | json.author person, :name 184 | end 185 | 186 | assert_equal 'Pavel', result['author']['name'] 187 | assert_equal 27, result['author']['age'] 188 | end 189 | 190 | test 'arrays are additive' do 191 | result = jbuild do |json| 192 | json.array! %w[foo] 193 | json.array! %w[bar] 194 | end 195 | 196 | assert_equal %w[foo bar], result 197 | end 198 | 199 | test 'nesting multiple children with block' do 200 | result = jbuild do |json| 201 | json.comments do 202 | json.child! { json.content 'hello' } 203 | json.child! { json.content 'world' } 204 | end 205 | end 206 | 207 | assert_equal 'hello', result['comments'].first['content'] 208 | assert_equal 'world', result['comments'].second['content'] 209 | end 210 | 211 | test 'nesting single child with inline extract' do 212 | person = Person.new('David', 32) 213 | 214 | result = jbuild do |json| 215 | json.author person, :name, :age 216 | end 217 | 218 | assert_equal 'David', result['author']['name'] 219 | assert_equal 32, result['author']['age'] 220 | end 221 | 222 | test 'nesting multiple children from array' do 223 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ] 224 | 225 | result = jbuild do |json| 226 | json.comments comments, :content 227 | end 228 | 229 | assert_equal ['content'], result['comments'].first.keys 230 | assert_equal 'hello', result['comments'].first['content'] 231 | assert_equal 'world', result['comments'].second['content'] 232 | end 233 | 234 | test 'nesting multiple children from array when child array is empty' do 235 | comments = [] 236 | 237 | result = jbuild do |json| 238 | json.name 'Parent' 239 | json.comments comments, :content 240 | end 241 | 242 | assert_equal 'Parent', result['name'] 243 | assert_equal [], result['comments'] 244 | end 245 | 246 | test 'nesting multiple children from array with inline loop' do 247 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ] 248 | 249 | result = jbuild do |json| 250 | json.comments comments do |comment| 251 | json.content comment.content 252 | end 253 | end 254 | 255 | assert_equal ['content'], result['comments'].first.keys 256 | assert_equal 'hello', result['comments'].first['content'] 257 | assert_equal 'world', result['comments'].second['content'] 258 | end 259 | 260 | test 'handles nil-collections as empty arrays' do 261 | result = jbuild do |json| 262 | json.comments nil do |comment| 263 | json.content comment.content 264 | end 265 | end 266 | 267 | assert_equal [], result['comments'] 268 | end 269 | 270 | test 'nesting multiple children from a non-Enumerable that responds to #map' do 271 | comments = NonEnumerable.new([ Comment.new('hello', 1), Comment.new('world', 2) ]) 272 | 273 | result = jbuild do |json| 274 | json.comments comments, :content 275 | end 276 | 277 | assert_equal ['content'], result['comments'].first.keys 278 | assert_equal 'hello', result['comments'].first['content'] 279 | assert_equal 'world', result['comments'].second['content'] 280 | end 281 | 282 | test 'nesting multiple chilren from a non-Enumerable that responds to #map with inline loop' do 283 | comments = NonEnumerable.new([ Comment.new('hello', 1), Comment.new('world', 2) ]) 284 | 285 | result = jbuild do |json| 286 | json.comments comments do |comment| 287 | json.content comment.content 288 | end 289 | end 290 | 291 | assert_equal ['content'], result['comments'].first.keys 292 | assert_equal 'hello', result['comments'].first['content'] 293 | assert_equal 'world', result['comments'].second['content'] 294 | end 295 | 296 | test 'array! casts array-like objects to array before merging' do 297 | wrapped_array = VeryBasicWrapper.new(%w[foo bar]) 298 | 299 | result = jbuild do |json| 300 | json.array! wrapped_array 301 | end 302 | 303 | assert_equal %w[foo bar], result 304 | end 305 | 306 | test 'nesting multiple children from array with inline loop on root' do 307 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ] 308 | 309 | result = jbuild do |json| 310 | json.call(comments) do |comment| 311 | json.content comment.content 312 | end 313 | end 314 | 315 | assert_equal 'hello', result.first['content'] 316 | assert_equal 'world', result.second['content'] 317 | end 318 | 319 | test 'array nested inside nested hash' do 320 | result = jbuild do |json| 321 | json.author do 322 | json.name 'David' 323 | json.age 32 324 | 325 | json.comments do 326 | json.child! { json.content 'hello' } 327 | json.child! { json.content 'world' } 328 | end 329 | end 330 | end 331 | 332 | assert_equal 'hello', result['author']['comments'].first['content'] 333 | assert_equal 'world', result['author']['comments'].second['content'] 334 | end 335 | 336 | test 'array nested inside array' do 337 | result = jbuild do |json| 338 | json.comments do 339 | json.child! do 340 | json.authors do 341 | json.child! do 342 | json.name 'david' 343 | end 344 | end 345 | end 346 | end 347 | end 348 | 349 | assert_equal 'david', result['comments'].first['authors'].first['name'] 350 | end 351 | 352 | test 'directly set an array nested in another array' do 353 | data = [ { :department => 'QA', :not_in_json => 'hello', :names => ['John', 'David'] } ] 354 | 355 | result = jbuild do |json| 356 | json.array! data do |object| 357 | json.department object[:department] 358 | json.names do 359 | json.array! object[:names] 360 | end 361 | end 362 | end 363 | 364 | assert_equal 'David', result[0]['names'].last 365 | assert !result[0].key?('not_in_json') 366 | end 367 | 368 | test 'nested jbuilder objects' do 369 | to_nest = Jbuilder.new{ |json| json.nested_value 'Nested Test' } 370 | 371 | result = jbuild do |json| 372 | json.value 'Test' 373 | json.nested to_nest 374 | end 375 | 376 | expected = {'value' => 'Test', 'nested' => {'nested_value' => 'Nested Test'}} 377 | assert_equal expected, result 378 | end 379 | 380 | test 'nested jbuilder object via set!' do 381 | to_nest = Jbuilder.new{ |json| json.nested_value 'Nested Test' } 382 | 383 | result = jbuild do |json| 384 | json.value 'Test' 385 | json.set! :nested, to_nest 386 | end 387 | 388 | expected = {'value' => 'Test', 'nested' => {'nested_value' => 'Nested Test'}} 389 | assert_equal expected, result 390 | end 391 | 392 | test 'top-level array' do 393 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ] 394 | 395 | result = jbuild do |json| 396 | json.array! comments do |comment| 397 | json.content comment.content 398 | end 399 | end 400 | 401 | assert_equal 'hello', result.first['content'] 402 | assert_equal 'world', result.second['content'] 403 | end 404 | 405 | test 'it allows using next in array block to skip value' do 406 | comments = [ Comment.new('hello', 1), Comment.new('skip', 2), Comment.new('world', 3) ] 407 | result = jbuild do |json| 408 | json.array! comments do |comment| 409 | next if comment.id == 2 410 | json.content comment.content 411 | end 412 | end 413 | 414 | assert_equal 2, result.length 415 | assert_equal 'hello', result.first['content'] 416 | assert_equal 'world', result.second['content'] 417 | end 418 | 419 | test 'extract attributes directly from array' do 420 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ] 421 | 422 | result = jbuild do |json| 423 | json.array! comments, :content, :id 424 | end 425 | 426 | assert_equal 'hello', result.first['content'] 427 | assert_equal 1, result.first['id'] 428 | assert_equal 'world', result.second['content'] 429 | assert_equal 2, result.second['id'] 430 | end 431 | 432 | test 'empty top-level array' do 433 | comments = [] 434 | 435 | result = jbuild do |json| 436 | json.array! comments do |comment| 437 | json.content comment.content 438 | end 439 | end 440 | 441 | assert_equal [], result 442 | end 443 | 444 | test 'dynamically set a key/value' do 445 | result = jbuild do |json| 446 | json.set! :each, 'stuff' 447 | end 448 | 449 | assert_equal 'stuff', result['each'] 450 | end 451 | 452 | test 'dynamically set a key/nested child with block' do 453 | result = jbuild do |json| 454 | json.set! :author do 455 | json.name 'David' 456 | json.age 32 457 | end 458 | end 459 | 460 | assert_equal 'David', result['author']['name'] 461 | assert_equal 32, result['author']['age'] 462 | end 463 | 464 | test 'dynamically sets a collection' do 465 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ] 466 | 467 | result = jbuild do |json| 468 | json.set! :comments, comments, :content 469 | end 470 | 471 | assert_equal ['content'], result['comments'].first.keys 472 | assert_equal 'hello', result['comments'].first['content'] 473 | assert_equal 'world', result['comments'].second['content'] 474 | end 475 | 476 | test 'query like object' do 477 | result = jbuild do |json| 478 | json.relations RelationMock.new, :name, :age 479 | end 480 | 481 | assert_equal 2, result['relations'].length 482 | assert_equal 'Bob', result['relations'][0]['name'] 483 | assert_equal 50, result['relations'][1]['age'] 484 | end 485 | 486 | test 'initialize via options hash' do 487 | jbuilder = Jbuilder.new(key_formatter: 1, ignore_nil: 2) 488 | assert_equal 1, jbuilder.instance_eval{ @key_formatter } 489 | assert_equal 2, jbuilder.instance_eval{ @ignore_nil } 490 | end 491 | 492 | test 'key_format! with parameter' do 493 | result = jbuild do |json| 494 | json.key_format! camelize: [:lower] 495 | json.camel_style 'for JS' 496 | end 497 | 498 | assert_equal ['camelStyle'], result.keys 499 | end 500 | 501 | test 'key_format! with parameter not as an array' do 502 | result = jbuild do |json| 503 | json.key_format! :camelize => :lower 504 | json.camel_style 'for JS' 505 | end 506 | 507 | assert_equal ['camelStyle'], result.keys 508 | end 509 | 510 | test 'key_format! propagates to child elements' do 511 | result = jbuild do |json| 512 | json.key_format! :upcase 513 | json.level1 'one' 514 | json.level2 do 515 | json.value 'two' 516 | end 517 | end 518 | 519 | assert_equal 'one', result['LEVEL1'] 520 | assert_equal 'two', result['LEVEL2']['VALUE'] 521 | end 522 | 523 | test 'key_format! resets after child element' do 524 | result = jbuild do |json| 525 | json.level2 do 526 | json.key_format! :upcase 527 | json.value 'two' 528 | end 529 | json.level1 'one' 530 | end 531 | 532 | assert_equal 'two', result['level2']['VALUE'] 533 | assert_equal 'one', result['level1'] 534 | end 535 | 536 | test 'key_format! with no parameter' do 537 | result = jbuild do |json| 538 | json.key_format! :upcase 539 | json.lower 'Value' 540 | end 541 | 542 | assert_equal ['LOWER'], result.keys 543 | end 544 | 545 | test 'key_format! with multiple steps' do 546 | result = jbuild do |json| 547 | json.key_format! :upcase, :pluralize 548 | json.pill 'foo' 549 | end 550 | 551 | assert_equal ['PILLs'], result.keys 552 | end 553 | 554 | test 'key_format! with lambda/proc' do 555 | result = jbuild do |json| 556 | json.key_format! ->(key){ key + ' and friends' } 557 | json.oats 'foo' 558 | end 559 | 560 | assert_equal ['oats and friends'], result.keys 561 | end 562 | 563 | test 'default key_format!' do 564 | Jbuilder.key_format camelize: :lower 565 | result = jbuild{ |json| json.camel_style 'for JS' } 566 | assert_equal ['camelStyle'], result.keys 567 | Jbuilder.send :class_variable_set, '@@key_formatter', Jbuilder::KeyFormatter.new 568 | end 569 | 570 | test 'do not use default key formatter directly' do 571 | jbuild{ |json| json.key 'value' } 572 | cache = Jbuilder.send(:class_variable_get, '@@key_formatter').instance_variable_get('@cache') 573 | assert_empty cache 574 | end 575 | 576 | test 'ignore_nil! without a parameter' do 577 | result = jbuild do |json| 578 | json.ignore_nil! 579 | json.test nil 580 | end 581 | 582 | assert_empty result.keys 583 | end 584 | 585 | test 'ignore_nil! with parameter' do 586 | result = jbuild do |json| 587 | json.ignore_nil! true 588 | json.name 'Bob' 589 | json.dne nil 590 | end 591 | 592 | assert_equal ['name'], result.keys 593 | 594 | result = jbuild do |json| 595 | json.ignore_nil! false 596 | json.name 'Bob' 597 | json.dne nil 598 | end 599 | 600 | assert_equal ['name', 'dne'], result.keys 601 | end 602 | 603 | test 'default ignore_nil!' do 604 | Jbuilder.ignore_nil 605 | 606 | result = jbuild do |json| 607 | json.name 'Bob' 608 | json.dne nil 609 | end 610 | 611 | assert_equal ['name'], result.keys 612 | Jbuilder.send(:class_variable_set, '@@ignore_nil', false) 613 | end 614 | 615 | test 'nil!' do 616 | result = jbuild do |json| 617 | json.key 'value' 618 | json.nil! 619 | end 620 | 621 | assert_nil result 622 | end 623 | 624 | test 'null!' do 625 | result = jbuild do |json| 626 | json.key 'value' 627 | json.null! 628 | end 629 | 630 | assert_nil result 631 | end 632 | 633 | test 'null! in a block' do 634 | result = jbuild do |json| 635 | json.author do 636 | json.name 'David' 637 | end 638 | 639 | json.author do 640 | json.null! 641 | end 642 | end 643 | 644 | assert result.key?('author') 645 | assert_nil result['author'] 646 | end 647 | 648 | test 'throws NullError when trying to add properties to null' do 649 | json = Jbuilder.new 650 | json.null! 651 | assert_raise Jbuilder::NullError do 652 | json.foo 'bar' 653 | end 654 | end 655 | 656 | test 'throws NullError when trying to add properties to null using block syntax' do 657 | assert_raise Jbuilder::NullError do 658 | jbuild do |json| 659 | json.author do 660 | json.null! 661 | end 662 | 663 | json.author do 664 | json.name "Pavel" 665 | end 666 | end 667 | end 668 | end 669 | end 670 | --------------------------------------------------------------------------------