├── .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 | [][travis]
4 | [][gem]
5 | [][codeclimate]
6 | [][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 |
--------------------------------------------------------------------------------