├── .github
└── workflows
│ └── ruby.yml
├── .gitignore
├── Appraisals
├── CONTRIBUTING.md
├── Gemfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── bin
├── release
└── test
├── gemfiles
├── rails_7_0.gemfile
├── rails_7_1.gemfile
├── rails_8_0.gemfile
└── rails_head.gemfile
├── jbuilder.gemspec
├── lib
├── generators
│ └── rails
│ │ ├── jbuilder_generator.rb
│ │ ├── scaffold_controller_generator.rb
│ │ └── templates
│ │ ├── api_controller.rb
│ │ ├── controller.rb
│ │ ├── index.json.jbuilder
│ │ ├── partial.json.jbuilder
│ │ └── show.json.jbuilder
├── jbuilder.rb
└── jbuilder
│ ├── blank.rb
│ ├── collection_renderer.rb
│ ├── errors.rb
│ ├── jbuilder.rb
│ ├── jbuilder_dependency_tracker.rb
│ ├── jbuilder_template.rb
│ ├── key_formatter.rb
│ ├── railtie.rb
│ └── version.rb
└── test
├── jbuilder_dependency_tracker_test.rb
├── jbuilder_generator_test.rb
├── jbuilder_template_test.rb
├── jbuilder_test.rb
├── scaffold_api_controller_generator_test.rb
├── scaffold_controller_generator_test.rb
└── test_helper.rb
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | name: Ruby test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | name: Ruby ${{ matrix.ruby }} (${{ matrix.gemfile }})
8 | runs-on: ubuntu-latest
9 | continue-on-error: ${{ matrix.gemfile == 'rails_head' }}
10 | env:
11 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
12 | BUNDLE_JOBS: 4
13 | BUNDLE_RETRY: 3
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | ruby:
18 | - "3.0"
19 | - "3.1"
20 | - "3.2"
21 | - "3.3"
22 | - "3.4"
23 |
24 | gemfile:
25 | - "rails_7_0"
26 | - "rails_7_1"
27 | - "rails_8_0"
28 | - "rails_head"
29 |
30 | exclude:
31 | - ruby: '3.0'
32 | gemfile: rails_8_0
33 | - ruby: '3.0'
34 | gemfile: rails_head
35 | - ruby: '3.1'
36 | gemfile: rails_8_0
37 | - ruby: '3.1'
38 | gemfile: rails_head
39 | - ruby: '3.4'
40 | gemfile: rails_7_0
41 |
42 | steps:
43 | - uses: actions/checkout@v4
44 |
45 | - uses: ruby/setup-ruby@v1
46 | with:
47 | ruby-version: ${{ matrix.ruby }}
48 | bundler-cache: true
49 |
50 | - name: Ruby test
51 | run: bundle exec rake
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | /log
3 | gemfiles/.bundle
4 | gemfiles/*.lock
5 | Gemfile.lock
6 | .ruby-version
7 | pkg
8 | *.gem
9 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "rails-7-0" do
2 | gem "rails", "~> 7.0.0"
3 | gem "concurrent-ruby", "< 1.3.5" # to avoid problem described in https://github.com/rails/rails/pull/54264
4 | end
5 |
6 | appraise "rails-7-1" do
7 | gem "rails", "~> 7.1.0"
8 | end
9 |
10 | appraise "rails-8-0" do
11 | gem "rails", "~> 8.0.0"
12 | end
13 |
14 | appraise "rails-head" do
15 | gem "rails", github: "rails/rails", branch: "main"
16 | end
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to Jbuilder
2 | =====================
3 |
4 | [][test]
5 | [][gem]
6 | [][codeclimate]
7 |
8 | [test]: https://github.com/rails/jbuilder/actions?query=branch%3Amaster
9 | [gem]: https://rubygems.org/gems/jbuilder
10 | [codeclimate]: https://codeclimate.com/github/rails/jbuilder
11 |
12 | Jbuilder is work of [many contributors](https://github.com/rails/jbuilder/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/rails/jbuilder/pulls), [propose features and discuss issues](https://github.com/rails/jbuilder/issues).
13 |
14 | #### Fork the Project
15 |
16 | Fork the [project on GitHub](https://github.com/rails/jbuilder) and check out your copy.
17 |
18 | ```
19 | git clone https://github.com/contributor/jbuilder.git
20 | cd jbuilder
21 | git remote add upstream https://github.com/rails/jbuilder.git
22 | ```
23 |
24 | #### Create a Topic Branch
25 |
26 | Make sure your fork is up-to-date and create a topic branch for your feature or bug fix.
27 |
28 | ```
29 | git checkout master
30 | git pull upstream master
31 | git checkout -b my-feature-branch
32 | ```
33 |
34 | #### Bundle Install and Test
35 |
36 | Ensure that you can build the project and run tests using `bin/test`.
37 |
38 | #### Write Tests
39 |
40 | Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [test](test).
41 |
42 | We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix.
43 |
44 | #### Write Code
45 |
46 | Implement your feature or bug fix.
47 |
48 | Make sure that `appraisal rake test` completes without errors.
49 |
50 | #### Write Documentation
51 |
52 | Document any external behavior in the [README](README.md).
53 |
54 | #### Commit Changes
55 |
56 | Make sure git knows your name and email address:
57 |
58 | ```
59 | git config --global user.name "Your Name"
60 | git config --global user.email "contributor@example.com"
61 | ```
62 |
63 | Writing good commit logs is important. A commit log should describe what changed and why.
64 |
65 | ```
66 | git add ...
67 | git commit
68 | ```
69 |
70 | #### Push
71 |
72 | ```
73 | git push origin my-feature-branch
74 | ```
75 |
76 | #### Make a Pull Request
77 |
78 | Visit your forked repo and click the 'New pull request' button. Select your feature branch, fill out the form, and click the 'Create pull request' button. Pull requests are usually reviewed within a few days.
79 |
80 | #### Rebase
81 |
82 | If you've been working on a change for a while, rebase with upstream/master.
83 |
84 | ```
85 | git fetch upstream
86 | git rebase upstream/master
87 | git push origin my-feature-branch -f
88 | ```
89 |
90 | #### Check on Your Pull Request
91 |
92 | Go back to your pull request after a few minutes and see whether it passed muster with GitHub Actions. Everything should look green, otherwise fix issues and amend your commit as described above.
93 |
94 | #### Be Patient
95 |
96 | It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang in there!
97 |
98 | #### Thank You
99 |
100 | Please do know that we really appreciate and value your time and work. We love you, really.
101 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | gem "rake"
6 | gem "mocha", require: false
7 | gem "appraisal"
8 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2018 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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jbuilder
2 |
3 | Jbuilder gives you a simple DSL for declaring JSON structures that beats
4 | manipulating giant hash structures. This is particularly helpful when the
5 | generation process is fraught with conditionals and loops. Here's a simple
6 | example:
7 |
8 | ```ruby
9 | # app/views/messages/show.json.jbuilder
10 |
11 | json.content format_content(@message.content)
12 | json.(@message, :created_at, :updated_at)
13 |
14 | json.author do
15 | json.name @message.creator.name.familiar
16 | json.email_address @message.creator.email_address_with_name
17 | json.url url_for(@message.creator, format: :json)
18 | end
19 |
20 | if current_user.admin?
21 | json.visitors calculate_visitors(@message)
22 | end
23 |
24 | json.comments @message.comments, :content, :created_at
25 |
26 | json.attachments @message.attachments do |attachment|
27 | json.filename attachment.filename
28 | json.url url_for(attachment)
29 | end
30 | ```
31 |
32 | This will build the following structure:
33 |
34 | ```javascript
35 | {
36 | "content": "
This is serious monkey business
",
37 | "created_at": "2011-10-29T20:45:28-05:00",
38 | "updated_at": "2011-10-29T20:45:28-05:00",
39 |
40 | "author": {
41 | "name": "David H.",
42 | "email_address": "'David Heinemeier Hansson' ",
43 | "url": "http://example.com/users/1-david.json"
44 | },
45 |
46 | "visitors": 15,
47 |
48 | "comments": [
49 | { "content": "Hello everyone!", "created_at": "2011-10-29T20:45:28-05:00" },
50 | { "content": "To you my good sir!", "created_at": "2011-10-29T20:47:28-05:00" }
51 | ],
52 |
53 | "attachments": [
54 | { "filename": "forecast.xls", "url": "http://example.com/downloads/forecast.xls" },
55 | { "filename": "presentation.pdf", "url": "http://example.com/downloads/presentation.pdf" }
56 | ]
57 | }
58 | ```
59 |
60 | ## Dynamically Defined Attributes
61 |
62 | To define attribute and structure names dynamically, use the `set!` method:
63 |
64 | ```ruby
65 | json.set! :author do
66 | json.set! :name, 'David'
67 | end
68 |
69 | # => {"author": { "name": "David" }}
70 | ```
71 |
72 | ## Merging Existing Hash or Array
73 |
74 | To merge existing hash or array to current context:
75 |
76 | ```ruby
77 | hash = { author: { name: "David" } }
78 | json.post do
79 | json.title "Merge HOWTO"
80 | json.merge! hash
81 | end
82 |
83 | # => "post": { "title": "Merge HOWTO", "author": { "name": "David" } }
84 | ```
85 |
86 | ## Top Level Arrays
87 |
88 | Top level arrays can be handled directly. Useful for index and other collection actions.
89 |
90 | ```ruby
91 | # @comments = @post.comments
92 |
93 | json.array! @comments do |comment|
94 | next if comment.marked_as_spam_by?(current_user)
95 |
96 | json.body comment.body
97 | json.author do
98 | json.first_name comment.author.first_name
99 | json.last_name comment.author.last_name
100 | end
101 | end
102 |
103 | # => [ { "body": "great post...", "author": { "first_name": "Joe", "last_name": "Bloe" }} ]
104 | ```
105 |
106 | ## Array Attributes
107 |
108 | You can also extract attributes from array directly.
109 |
110 | ```ruby
111 | # @people = People.all
112 |
113 | json.array! @people, :id, :name
114 |
115 | # => [ { "id": 1, "name": "David" }, { "id": 2, "name": "Jamie" } ]
116 | ```
117 |
118 | ## Plain Arrays
119 |
120 | To make a plain array without keys, construct and pass in a standard Ruby array.
121 |
122 | ```ruby
123 | my_array = %w(David Jamie)
124 |
125 | json.people my_array
126 |
127 | # => "people": [ "David", "Jamie" ]
128 | ```
129 |
130 | ## Child Objects
131 |
132 | You don't always have or need a collection when building an array.
133 |
134 | ```ruby
135 | json.people do
136 | json.child! do
137 | json.id 1
138 | json.name 'David'
139 | end
140 | json.child! do
141 | json.id 2
142 | json.name 'Jamie'
143 | end
144 | end
145 |
146 | # => { "people": [ { "id": 1, "name": "David" }, { "id": 2, "name": "Jamie" } ] }
147 | ```
148 |
149 | ## Nested Jbuilder Objects
150 |
151 | Jbuilder objects can be directly nested inside each other. Useful for composing objects.
152 |
153 | ```ruby
154 | class Person
155 | # ... Class Definition ... #
156 | def to_builder
157 | Jbuilder.new do |person|
158 | person.(self, :name, :age)
159 | end
160 | end
161 | end
162 |
163 | class Company
164 | # ... Class Definition ... #
165 | def to_builder
166 | Jbuilder.new do |company|
167 | company.name name
168 | company.president president.to_builder
169 | end
170 | end
171 | end
172 |
173 | company = Company.new('Doodle Corp', Person.new('John Stobs', 58))
174 | company.to_builder.target!
175 |
176 | # => {"name":"Doodle Corp","president":{"name":"John Stobs","age":58}}
177 | ```
178 |
179 | ## Rails Integration
180 |
181 | You can either use Jbuilder stand-alone or directly as an ActionView template
182 | language. When required in Rails, you can create views à la show.json.jbuilder
183 | (the json is already yielded):
184 |
185 | ```ruby
186 | # Any helpers available to views are available to the builder
187 | json.content format_content(@message.content)
188 | json.(@message, :created_at, :updated_at)
189 |
190 | json.author do
191 | json.name @message.creator.name.familiar
192 | json.email_address @message.creator.email_address_with_name
193 | json.url url_for(@message.creator, format: :json)
194 | end
195 |
196 | if current_user.admin?
197 | json.visitors calculate_visitors(@message)
198 | end
199 | ```
200 |
201 | ## Partials
202 |
203 | You can use partials as well. The following will render the file
204 | `views/comments/_comments.json.jbuilder`, and set a local variable
205 | `comments` with all this message's comments, which you can use inside
206 | the partial.
207 |
208 | ```ruby
209 | json.partial! 'comments/comments', comments: @message.comments
210 | ```
211 |
212 | It's also possible to render collections of partials:
213 |
214 | ```ruby
215 | json.array! @posts, partial: 'posts/post', as: :post
216 |
217 | # or
218 | json.partial! 'posts/post', collection: @posts, as: :post
219 |
220 | # or
221 | json.partial! partial: 'posts/post', collection: @posts, as: :post
222 |
223 | # or
224 | json.comments @post.comments, partial: 'comments/comment', as: :comment
225 | ```
226 |
227 | The `as: :some_symbol` is used with partials. It will take care of mapping the passed in object to a variable for the
228 | partial. If the value is a collection either implicitly or explicitly by using the `collection:` option, then each
229 | value of the collection is passed to the partial as the variable `some_symbol`. If the value is a singular object,
230 | then the object is passed to the partial as the variable `some_symbol`.
231 |
232 | Be sure not to confuse the `as:` option to mean nesting of the partial. For example:
233 |
234 | ```ruby
235 | # Use the default `views/comments/_comment.json.jbuilder`, putting @comment as the comment local variable.
236 | # Note, `comment` attributes are "inlined".
237 | json.partial! @comment, as: :comment
238 | ```
239 |
240 | is quite different from:
241 |
242 | ```ruby
243 | # comment attributes are nested under a "comment" property
244 | json.comment do
245 | json.partial! "/comments/comment.json.jbuilder", comment: @comment
246 | end
247 | ```
248 |
249 | You can pass any objects into partial templates with or without `:locals` option.
250 |
251 | ```ruby
252 | json.partial! 'sub_template', locals: { user: user }
253 |
254 | # or
255 |
256 | json.partial! 'sub_template', user: user
257 | ```
258 |
259 | ## Null Values
260 |
261 | You can explicitly make Jbuilder object return null if you want:
262 |
263 | ```ruby
264 | json.extract! @post, :id, :title, :content, :published_at
265 | json.author do
266 | if @post.anonymous?
267 | json.null! # or json.nil!
268 | else
269 | json.first_name @post.author_first_name
270 | json.last_name @post.author_last_name
271 | end
272 | end
273 | ```
274 |
275 | To prevent Jbuilder from including null values in the output, you can use the `ignore_nil!` method:
276 |
277 | ```ruby
278 | json.ignore_nil!
279 | json.foo nil
280 | json.bar "bar"
281 | # => { "bar": "bar" }
282 | ```
283 |
284 | ## Caching
285 |
286 | Fragment caching is supported, it uses `Rails.cache` and works like caching in
287 | HTML templates:
288 |
289 | ```ruby
290 | json.cache! ['v1', @person], expires_in: 10.minutes do
291 | json.extract! @person, :name, :age
292 | end
293 | ```
294 |
295 | You can also conditionally cache a block by using `cache_if!` like this:
296 |
297 | ```ruby
298 | json.cache_if! !admin?, ['v1', @person], expires_in: 10.minutes do
299 | json.extract! @person, :name, :age
300 | end
301 | ```
302 |
303 | Aside from that, the `:cached` options on collection rendering is available on Rails >= 6.0. This will cache the
304 | rendered results effectively using the multi fetch feature.
305 |
306 | ```ruby
307 | json.array! @posts, partial: "posts/post", as: :post, cached: true
308 |
309 | # or:
310 | json.comments @post.comments, partial: "comments/comment", as: :comment, cached: true
311 | ```
312 |
313 | If your collection cache depends on multiple sources (try to avoid this to keep things simple), you can name all these dependencies as part of a block that returns an array:
314 |
315 | ```ruby
316 | json.array! @posts, partial: "posts/post", as: :post, cached: -> post { [post, current_user] }
317 | ```
318 |
319 | This will include both records as part of the cache key and updating either of them will expire the cache.
320 |
321 | ## Formatting Keys
322 |
323 | Keys can be auto formatted using `key_format!`, this can be used to convert
324 | keynames from the standard ruby_format to camelCase:
325 |
326 | ```ruby
327 | json.key_format! camelize: :lower
328 | json.first_name 'David'
329 |
330 | # => { "firstName": "David" }
331 | ```
332 |
333 | You can set this globally with the class method `key_format` (from inside your
334 | environment.rb for example):
335 |
336 | ```ruby
337 | Jbuilder.key_format camelize: :lower
338 | ```
339 |
340 | By default, key format is not applied to keys of hashes that are
341 | passed to methods like `set!`, `array!` or `merge!`. You can opt into
342 | deeply transforming these as well:
343 |
344 | ```ruby
345 | json.key_format! camelize: :lower
346 | json.deep_format_keys!
347 | json.settings([{some_value: "abc"}])
348 |
349 | # => { "settings": [{ "someValue": "abc" }]}
350 | ```
351 |
352 | You can set this globally with the class method `deep_format_keys` (from inside your
353 | environment.rb for example):
354 |
355 | ```ruby
356 | Jbuilder.deep_format_keys true
357 | ```
358 |
359 | ## Testing JBuilder Response body with RSpec
360 |
361 | To test the response body of your controller spec, enable `render_views` in your RSpec context. This [configuration](https://rspec.info/features/6-0/rspec-rails/controller-specs/render-views) renders the views in a controller test.
362 |
363 | ## Contributing to Jbuilder
364 |
365 | Jbuilder is the work of many contributors. You're encouraged to submit pull requests, propose
366 | features and discuss issues.
367 |
368 | See [CONTRIBUTING](CONTRIBUTING.md).
369 |
370 | ## License
371 |
372 | Jbuilder is released under the [MIT License](http://www.opensource.org/licenses/MIT).
373 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 | require "bundler/gem_tasks"
3 | require "rake/testtask"
4 |
5 | if !ENV["APPRAISAL_INITIALIZED"] && !ENV["CI"]
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 | test.test_files = FileList["test/*_test.rb"]
16 | end
17 |
18 | task default: :test
19 | end
20 |
--------------------------------------------------------------------------------
/bin/release:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | VERSION=$1
4 |
5 | printf "class Jbuilder\n VERSION = \"$VERSION\"\nend\n" > ./lib/jbuilder/version.rb
6 | bundle
7 | git add lib/jbuilder/version.rb
8 | git commit -m "Bump version for $VERSION"
9 | git push
10 | git tag v$VERSION
11 | git push --tags
12 | gem build jbuilder.gemspec
13 | gem push "jbuilder-$VERSION.gem" --host https://rubygems.org
14 | rm "jbuilder-$VERSION.gem"
15 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/bin/env bash
2 | set -e
3 |
4 | bundle install
5 | appraisal install
6 | appraisal rake test
7 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_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 "rails", "~> 7.0.0"
9 | gem "concurrent-ruby", "< 1.3.5"
10 |
11 | gemspec path: "../"
12 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_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 "rails", "~> 7.1.0"
9 |
10 | gemspec path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails_8_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 "rails", "~> 8.0.0"
9 |
10 | gemspec path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails_head.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 "rails", github: "rails/rails", branch: "main"
9 |
10 | gemspec path: "../"
11 |
--------------------------------------------------------------------------------
/jbuilder.gemspec:
--------------------------------------------------------------------------------
1 | require_relative "lib/jbuilder/version"
2 |
3 | Gem::Specification.new do |s|
4 | s.name = 'jbuilder'
5 | s.version = Jbuilder::VERSION
6 | s.authors = 'David Heinemeier Hansson'
7 | s.email = 'david@basecamp.com'
8 | s.summary = 'Create JSON structures via a Builder-style DSL'
9 | s.homepage = 'https://github.com/rails/jbuilder'
10 | s.license = 'MIT'
11 |
12 | s.required_ruby_version = '>= 2.2.2'
13 |
14 | s.add_dependency 'activesupport', '>= 5.0.0'
15 | s.add_dependency 'actionview', '>= 5.0.0'
16 |
17 | if RUBY_ENGINE == 'rbx'
18 | s.add_development_dependency('racc')
19 | s.add_development_dependency('json')
20 | s.add_development_dependency('rubysl')
21 | end
22 |
23 | s.files = `git ls-files`.split("\n")
24 | s.test_files = `git ls-files -- test/*`.split("\n")
25 |
26 | s.metadata = {
27 | "bug_tracker_uri" => "https://github.com/rails/jbuilder/issues",
28 | "changelog_uri" => "https://github.com/rails/jbuilder/releases/tag/v#{s.version}",
29 | "mailing_list_uri" => "https://discuss.rubyonrails.org/c/rubyonrails-talk",
30 | "source_code_uri" => "https://github.com/rails/jbuilder/tree/v#{s.version}",
31 | "rubygems_mfa_required" => "true",
32 | }
33 | end
34 |
--------------------------------------------------------------------------------
/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 | class_option :timestamps, type: :boolean, default: true
14 |
15 | def create_root_folder
16 | path = File.join('app/views', controller_file_path)
17 | empty_directory path unless File.directory?(path)
18 | end
19 |
20 | def copy_view_files
21 | %w(index show).each do |view|
22 | filename = filename_with_extensions(view)
23 | template filename, File.join('app/views', controller_file_path, filename)
24 | end
25 | template filename_with_extensions('partial'), File.join('app/views', controller_file_path, filename_with_extensions("_#{singular_table_name}"))
26 | end
27 |
28 |
29 | protected
30 | def attributes_names
31 | [:id] + super
32 | end
33 |
34 | def filename_with_extensions(name)
35 | [name, :json, :jbuilder] * '.'
36 | end
37 |
38 | def full_attributes_list
39 | if options[:timestamps]
40 | attributes_list(attributes_names + %w(created_at updated_at))
41 | else
42 | attributes_list(attributes_names)
43 | end
44 | end
45 |
46 | def attributes_list(attributes = attributes_names)
47 | if self.attributes.any? {|attr| attr.name == 'password' && attr.type == :digest}
48 | attributes = attributes.reject {|name| %w(password password_confirmation).include? name}
49 | end
50 |
51 | attributes.map { |a| ":#{a}"} * ', '
52 | end
53 |
54 | def virtual_attributes
55 | attributes.select {|name| name.respond_to?(:virtual?) && name.virtual? }
56 | end
57 |
58 | def partial_path_name
59 | [controller_file_path, singular_table_name].join("/")
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/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_paths << File.expand_path('../templates', __FILE__)
8 |
9 | hook_for :jbuilder, type: :boolean, default: true
10 |
11 | private
12 |
13 | def permitted_params
14 | attributes_names.map { |name| ":#{name}" }.join(', ')
15 | end unless private_method_defined? :permitted_params
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/generators/rails/templates/api_controller.rb:
--------------------------------------------------------------------------------
1 | <% if namespaced? -%>
2 | require_dependency "<%= namespaced_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: %i[ show 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 | # POST <%= route_url %>
21 | # POST <%= route_url %>.json
22 | def create
23 | @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
24 |
25 | if @<%= orm_instance.save %>
26 | render :show, status: :created, location: <%= "@#{singular_table_name}" %>
27 | else
28 | render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity
29 | end
30 | end
31 |
32 | # PATCH/PUT <%= route_url %>/1
33 | # PATCH/PUT <%= route_url %>/1.json
34 | def update
35 | if @<%= orm_instance.update("#{singular_table_name}_params") %>
36 | render :show, status: :ok, location: <%= "@#{singular_table_name}" %>
37 | else
38 | render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity
39 | end
40 | end
41 |
42 | # DELETE <%= route_url %>/1
43 | # DELETE <%= route_url %>/1.json
44 | def destroy
45 | @<%= orm_instance.destroy %>
46 | end
47 |
48 | private
49 | # Use callbacks to share common setup or constraints between actions.
50 | def set_<%= singular_table_name %>
51 | <%- if Rails::VERSION::MAJOR >= 8 -%>
52 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params.expect(:id)") %>
53 | <%- else -%>
54 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
55 | <%- end -%>
56 | end
57 |
58 | # Only allow a list of trusted parameters through.
59 | def <%= "#{singular_table_name}_params" %>
60 | <%- if attributes_names.empty? -%>
61 | params.fetch(<%= ":#{singular_table_name}" %>, {})
62 | <%- elsif Rails::VERSION::MAJOR >= 8 -%>
63 | params.expect(<%= singular_table_name %>: [ <%= permitted_params %> ])
64 | <%- else -%>
65 | params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
66 | <%- end -%>
67 | end
68 | end
69 | <% end -%>
70 |
--------------------------------------------------------------------------------
/lib/generators/rails/templates/controller.rb:
--------------------------------------------------------------------------------
1 | <% if namespaced? -%>
2 | require_dependency "<%= namespaced_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: %i[ show edit update destroy ]
8 |
9 | # GET <%= route_url %> or <%= route_url %>.json
10 | def index
11 | @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
12 | end
13 |
14 | # GET <%= route_url %>/1 or <%= route_url %>/1.json
15 | def show
16 | end
17 |
18 | # GET <%= route_url %>/new
19 | def new
20 | @<%= singular_table_name %> = <%= orm_class.build(class_name) %>
21 | end
22 |
23 | # GET <%= route_url %>/1/edit
24 | def edit
25 | end
26 |
27 | # POST <%= route_url %> or <%= route_url %>.json
28 | def create
29 | @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
30 |
31 | respond_to do |format|
32 | if @<%= orm_instance.save %>
33 | format.html { redirect_to <%= redirect_resource_name %>, notice: <%= %("#{human_name} was successfully created.") %> }
34 | format.json { render :show, status: :created, location: <%= "@#{singular_table_name}" %> }
35 | else
36 | format.html { render :new, status: :unprocessable_entity }
37 | format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
38 | end
39 | end
40 | end
41 |
42 | # PATCH/PUT <%= route_url %>/1 or <%= route_url %>/1.json
43 | def update
44 | respond_to do |format|
45 | if @<%= orm_instance.update("#{singular_table_name}_params") %>
46 | format.html { redirect_to <%= redirect_resource_name %>, notice: <%= %("#{human_name} was successfully updated.") %>, status: :see_other }
47 | format.json { render :show, status: :ok, location: <%= "@#{singular_table_name}" %> }
48 | else
49 | format.html { render :edit, status: :unprocessable_entity }
50 | format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
51 | end
52 | end
53 | end
54 |
55 | # DELETE <%= route_url %>/1 or <%= route_url %>/1.json
56 | def destroy
57 | @<%= orm_instance.destroy %>
58 |
59 | respond_to do |format|
60 | format.html { redirect_to <%= index_helper %>_path, notice: <%= %("#{human_name} was successfully destroyed.") %>, status: :see_other }
61 | format.json { head :no_content }
62 | end
63 | end
64 |
65 | private
66 | # Use callbacks to share common setup or constraints between actions.
67 | def set_<%= singular_table_name %>
68 | <%- if Rails::VERSION::MAJOR >= 8 -%>
69 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params.expect(:id)") %>
70 | <%- else -%>
71 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
72 | <%- end -%>
73 | end
74 |
75 | # Only allow a list of trusted parameters through.
76 | def <%= "#{singular_table_name}_params" %>
77 | <%- if attributes_names.empty? -%>
78 | params.fetch(<%= ":#{singular_table_name}" %>, {})
79 | <%- elsif Rails::VERSION::MAJOR >= 8 -%>
80 | params.expect(<%= singular_table_name %>: [ <%= permitted_params %> ])
81 | <%- else -%>
82 | params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
83 | <%- end -%>
84 | end
85 | end
86 | <% end -%>
87 |
--------------------------------------------------------------------------------
/lib/generators/rails/templates/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.array! @<%= plural_table_name %>, partial: "<%= partial_path_name %>", as: :<%= singular_table_name %>
2 |
--------------------------------------------------------------------------------
/lib/generators/rails/templates/partial.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! <%= singular_table_name %>, <%= full_attributes_list %>
2 | json.url <%= singular_table_name %>_url(<%= singular_table_name %>, format: :json)
3 | <%- virtual_attributes.each do |attribute| -%>
4 | <%- if attribute.type == :rich_text -%>
5 | json.<%= attribute.name %> <%= singular_table_name %>.<%= attribute.name %>.to_s
6 | <%- elsif attribute.type == :attachment -%>
7 | json.<%= attribute.name %> url_for(<%= singular_table_name %>.<%= attribute.name %>)
8 | <%- elsif attribute.type == :attachments -%>
9 | json.<%= attribute.name %> do
10 | json.array!(<%= singular_table_name %>.<%= attribute.name %>) do |<%= attribute.singular_name %>|
11 | json.id <%= attribute.singular_name %>.id
12 | json.url url_for(<%= attribute.singular_name %>)
13 | end
14 | end
15 | <%- end -%>
16 | <%- end -%>
17 |
--------------------------------------------------------------------------------
/lib/generators/rails/templates/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! "<%= partial_path_name %>", <%= singular_table_name %>: @<%= singular_table_name %>
2 |
--------------------------------------------------------------------------------
/lib/jbuilder.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'jbuilder/jbuilder'
3 | require 'jbuilder/blank'
4 | require 'jbuilder/key_formatter'
5 | require 'jbuilder/errors'
6 | require 'jbuilder/version'
7 | require 'json'
8 | require 'active_support/core_ext/hash/deep_merge'
9 |
10 | class Jbuilder
11 | @@key_formatter = nil
12 | @@ignore_nil = false
13 | @@deep_format_keys = false
14 |
15 | def initialize(options = {})
16 | @attributes = {}
17 |
18 | @key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
19 | @ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
20 | @deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)
21 |
22 | yield self if ::Kernel.block_given?
23 | end
24 |
25 | # Yields a builder and automatically turns the result into a JSON string
26 | def self.encode(*args, &block)
27 | new(*args, &block).target!
28 | end
29 |
30 | BLANK = Blank.new
31 |
32 | def set!(key, value = BLANK, *args, &block)
33 | result = if ::Kernel.block_given?
34 | if !_blank?(value)
35 | # json.comments @post.comments { |comment| ... }
36 | # { "comments": [ { ... }, { ... } ] }
37 | _scope{ array! value, &block }
38 | else
39 | # json.comments { ... }
40 | # { "comments": ... }
41 | _merge_block(key){ yield self }
42 | end
43 | elsif args.empty?
44 | if ::Jbuilder === value
45 | # json.age 32
46 | # json.person another_jbuilder
47 | # { "age": 32, "person": { ... }
48 | _format_keys(value.attributes!)
49 | else
50 | # json.age 32
51 | # { "age": 32 }
52 | _format_keys(value)
53 | end
54 | elsif _is_collection?(value)
55 | # json.comments @post.comments, :content, :created_at
56 | # { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
57 | _scope{ array! value, *args }
58 | else
59 | # json.author @post.creator, :name, :email_address
60 | # { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
61 | _merge_block(key){ extract! value, *args }
62 | end
63 |
64 | _set_value key, result
65 | end
66 |
67 | def method_missing(*args, &block)
68 | if ::Kernel.block_given?
69 | set!(*args, &block)
70 | else
71 | set!(*args)
72 | end
73 | end
74 |
75 | # Specifies formatting to be applied to the key. Passing in a name of a function
76 | # will cause that function to be called on the key. So :upcase will upper case
77 | # the key. You can also pass in lambdas for more complex transformations.
78 | #
79 | # Example:
80 | #
81 | # json.key_format! :upcase
82 | # json.author do
83 | # json.name "David"
84 | # json.age 32
85 | # end
86 | #
87 | # { "AUTHOR": { "NAME": "David", "AGE": 32 } }
88 | #
89 | # You can pass parameters to the method using a hash pair.
90 | #
91 | # json.key_format! camelize: :lower
92 | # json.first_name "David"
93 | #
94 | # { "firstName": "David" }
95 | #
96 | # Lambdas can also be used.
97 | #
98 | # json.key_format! ->(key){ "_" + key }
99 | # json.first_name "David"
100 | #
101 | # { "_first_name": "David" }
102 | #
103 | def key_format!(*args)
104 | @key_formatter = KeyFormatter.new(*args)
105 | end
106 |
107 | # Same as the instance method key_format! except sets the default.
108 | def self.key_format(*args)
109 | @@key_formatter = KeyFormatter.new(*args)
110 | end
111 |
112 | # If you want to skip adding nil values to your JSON hash. This is useful
113 | # for JSON clients that don't deal well with nil values, and would prefer
114 | # not to receive keys which have null values.
115 | #
116 | # Example:
117 | # json.ignore_nil! false
118 | # json.id User.new.id
119 | #
120 | # { "id": null }
121 | #
122 | # json.ignore_nil!
123 | # json.id User.new.id
124 | #
125 | # {}
126 | #
127 | def ignore_nil!(value = true)
128 | @ignore_nil = value
129 | end
130 |
131 | # Same as instance method ignore_nil! except sets the default.
132 | def self.ignore_nil(value = true)
133 | @@ignore_nil = value
134 | end
135 |
136 | # Deeply apply key format to nested hashes and arrays passed to
137 | # methods like set!, merge! or array!.
138 | #
139 | # Example:
140 | #
141 | # json.key_format! camelize: :lower
142 | # json.settings({some_value: "abc"})
143 | #
144 | # { "settings": { "some_value": "abc" }}
145 | #
146 | # json.key_format! camelize: :lower
147 | # json.deep_format_keys!
148 | # json.settings({some_value: "abc"})
149 | #
150 | # { "settings": { "someValue": "abc" }}
151 | #
152 | def deep_format_keys!(value = true)
153 | @deep_format_keys = value
154 | end
155 |
156 | # Same as instance method deep_format_keys! except sets the default.
157 | def self.deep_format_keys(value = true)
158 | @@deep_format_keys = value
159 | end
160 |
161 | # Turns the current element into an array and yields a builder to add a hash.
162 | #
163 | # Example:
164 | #
165 | # json.comments do
166 | # json.child! { json.content "hello" }
167 | # json.child! { json.content "world" }
168 | # end
169 | #
170 | # { "comments": [ { "content": "hello" }, { "content": "world" } ]}
171 | #
172 | # More commonly, you'd use the combined iterator, though:
173 | #
174 | # json.comments(@post.comments) do |comment|
175 | # json.content comment.formatted_content
176 | # end
177 | def child!
178 | @attributes = [] unless ::Array === @attributes
179 | @attributes << _scope{ yield self }
180 | end
181 |
182 | # Turns the current element into an array and iterates over the passed collection, adding each iteration as
183 | # an element of the resulting array.
184 | #
185 | # Example:
186 | #
187 | # json.array!(@people) do |person|
188 | # json.name person.name
189 | # json.age calculate_age(person.birthday)
190 | # end
191 | #
192 | # [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ]
193 | #
194 | # You can use the call syntax instead of an explicit extract! call:
195 | #
196 | # json.(@people) { |person| ... }
197 | #
198 | # It's generally only needed to use this method for top-level arrays. If you have named arrays, you can do:
199 | #
200 | # json.people(@people) do |person|
201 | # json.name person.name
202 | # json.age calculate_age(person.birthday)
203 | # end
204 | #
205 | # { "people": [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ] }
206 | #
207 | # If you omit the block then you can set the top level array directly:
208 | #
209 | # json.array! [1, 2, 3]
210 | #
211 | # [1,2,3]
212 | def array!(collection = [], *attributes, &block)
213 | array = if collection.nil?
214 | []
215 | elsif ::Kernel.block_given?
216 | _map_collection(collection, &block)
217 | elsif attributes.any?
218 | _map_collection(collection) { |element| extract! element, *attributes }
219 | else
220 | _format_keys(collection.to_a)
221 | end
222 |
223 | @attributes = _merge_values(@attributes, array)
224 | end
225 |
226 | # Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
227 | #
228 | # Example:
229 | #
230 | # @person = Struct.new(:name, :age).new('David', 32)
231 | #
232 | # or you can utilize a Hash
233 | #
234 | # @person = { name: 'David', age: 32 }
235 | #
236 | # json.extract! @person, :name, :age
237 | #
238 | # { "name": David", "age": 32 }, { "name": Jamie", "age": 31 }
239 | #
240 | # You can also use the call syntax instead of an explicit extract! call:
241 | #
242 | # json.(@person, :name, :age)
243 | def extract!(object, *attributes)
244 | if ::Hash === object
245 | _extract_hash_values(object, attributes)
246 | else
247 | _extract_method_values(object, attributes)
248 | end
249 | end
250 |
251 | def call(object, *attributes, &block)
252 | if ::Kernel.block_given?
253 | array! object, &block
254 | else
255 | extract! object, *attributes
256 | end
257 | end
258 |
259 | # Returns the nil JSON.
260 | def nil!
261 | @attributes = nil
262 | end
263 |
264 | alias_method :null!, :nil!
265 |
266 | # Returns the attributes of the current builder.
267 | def attributes!
268 | @attributes
269 | end
270 |
271 | # Merges hash, array, or Jbuilder instance into current builder.
272 | def merge!(object)
273 | hash_or_array = ::Jbuilder === object ? object.attributes! : object
274 | @attributes = _merge_values(@attributes, _format_keys(hash_or_array))
275 | end
276 |
277 | # Encodes the current builder as JSON.
278 | def target!
279 | @attributes.to_json
280 | end
281 |
282 | private
283 |
284 | def _extract_hash_values(object, attributes)
285 | attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
286 | end
287 |
288 | def _extract_method_values(object, attributes)
289 | attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
290 | end
291 |
292 | def _merge_block(key)
293 | current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK)
294 | ::Kernel.raise NullError.build(key) if current_value.nil?
295 | new_value = _scope{ yield self }
296 | _merge_values(current_value, new_value)
297 | end
298 |
299 | def _merge_values(current_value, updates)
300 | if _blank?(updates)
301 | current_value
302 | elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
303 | updates
304 | elsif ::Array === current_value && ::Array === updates
305 | current_value + updates
306 | elsif ::Hash === current_value && ::Hash === updates
307 | current_value.deep_merge(updates)
308 | else
309 | ::Kernel.raise MergeError.build(current_value, updates)
310 | end
311 | end
312 |
313 | def _key(key)
314 | @key_formatter ? @key_formatter.format(key) : key.to_s
315 | end
316 |
317 | def _format_keys(hash_or_array)
318 | return hash_or_array unless @deep_format_keys
319 |
320 | if ::Array === hash_or_array
321 | hash_or_array.map { |value| _format_keys(value) }
322 | elsif ::Hash === hash_or_array
323 | ::Hash[hash_or_array.collect { |k, v| [_key(k), _format_keys(v)] }]
324 | else
325 | hash_or_array
326 | end
327 | end
328 |
329 | def _set_value(key, value)
330 | ::Kernel.raise NullError.build(key) if @attributes.nil?
331 | ::Kernel.raise ArrayError.build(key) if ::Array === @attributes
332 | return if @ignore_nil && value.nil? or _blank?(value)
333 | @attributes = {} if _blank?
334 | @attributes[_key(key)] = value
335 | end
336 |
337 | def _map_collection(collection)
338 | collection.map do |element|
339 | _scope{ yield element }
340 | end - [BLANK]
341 | end
342 |
343 | def _scope
344 | parent_attributes, parent_formatter, parent_deep_format_keys = @attributes, @key_formatter, @deep_format_keys
345 | @attributes = BLANK
346 | yield
347 | @attributes
348 | ensure
349 | @attributes, @key_formatter, @deep_format_keys = parent_attributes, parent_formatter, parent_deep_format_keys
350 | end
351 |
352 | def _is_collection?(object)
353 | _object_respond_to?(object, :map, :count) && !(::Struct === object)
354 | end
355 |
356 | def _blank?(value=@attributes)
357 | BLANK == value
358 | end
359 |
360 | def _object_respond_to?(object, *methods)
361 | methods.all?{ |m| object.respond_to?(m) }
362 | end
363 | end
364 |
365 | require 'jbuilder/railtie' if defined?(Rails)
366 |
--------------------------------------------------------------------------------
/lib/jbuilder/blank.rb:
--------------------------------------------------------------------------------
1 | class Jbuilder
2 | class Blank
3 | def ==(other)
4 | super || Blank === other
5 | end
6 |
7 | def empty?
8 | true
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jbuilder/collection_renderer.rb:
--------------------------------------------------------------------------------
1 | require 'delegate'
2 | require 'active_support/concern'
3 | require 'action_view'
4 |
5 | begin
6 | require 'action_view/renderer/collection_renderer'
7 | rescue LoadError
8 | require 'action_view/renderer/partial_renderer'
9 | end
10 |
11 | class Jbuilder
12 | module CollectionRenderable # :nodoc:
13 | extend ActiveSupport::Concern
14 |
15 | class_methods do
16 | def supported?
17 | superclass.private_method_defined?(:build_rendered_template) && self.superclass.private_method_defined?(:build_rendered_collection)
18 | end
19 | end
20 |
21 | private
22 |
23 | def build_rendered_template(content, template, layout = nil)
24 | super(content || json.attributes!, template)
25 | end
26 |
27 | def build_rendered_collection(templates, _spacer)
28 | json.merge!(templates.map(&:body))
29 | end
30 |
31 | def json
32 | @options[:locals].fetch(:json)
33 | end
34 |
35 | class ScopedIterator < ::SimpleDelegator # :nodoc:
36 | include Enumerable
37 |
38 | def initialize(obj, scope)
39 | super(obj)
40 | @scope = scope
41 | end
42 |
43 | # Rails 6.0 support:
44 | def each
45 | return enum_for(:each) unless block_given?
46 |
47 | __getobj__.each do |object|
48 | @scope.call { yield(object) }
49 | end
50 | end
51 |
52 | # Rails 6.1 support:
53 | def each_with_info
54 | return enum_for(:each_with_info) unless block_given?
55 |
56 | __getobj__.each_with_info do |object, info|
57 | @scope.call { yield(object, info) }
58 | end
59 | end
60 | end
61 |
62 | private_constant :ScopedIterator
63 | end
64 |
65 | if defined?(::ActionView::CollectionRenderer)
66 | # Rails 6.1 support:
67 | class CollectionRenderer < ::ActionView::CollectionRenderer # :nodoc:
68 | include CollectionRenderable
69 |
70 | def initialize(lookup_context, options, &scope)
71 | super(lookup_context, options)
72 | @scope = scope
73 | end
74 |
75 | private
76 | def collection_with_template(view, template, layout, collection)
77 | super(view, template, layout, ScopedIterator.new(collection, @scope))
78 | end
79 | end
80 | else
81 | # Rails 6.0 support:
82 | class CollectionRenderer < ::ActionView::PartialRenderer # :nodoc:
83 | include CollectionRenderable
84 |
85 | def initialize(lookup_context, options, &scope)
86 | super(lookup_context)
87 | @options = options
88 | @scope = scope
89 | end
90 |
91 | def render_collection_with_partial(collection, partial, context, block)
92 | render(context, @options.merge(collection: collection, partial: partial), block)
93 | end
94 |
95 | private
96 | def collection_without_template(view)
97 | @collection = ScopedIterator.new(@collection, @scope)
98 |
99 | super(view)
100 | end
101 |
102 | def collection_with_template(view, template)
103 | @collection = ScopedIterator.new(@collection, @scope)
104 |
105 | super(view, template)
106 | end
107 | end
108 | end
109 |
110 | class EnumerableCompat < ::SimpleDelegator
111 | # Rails 6.1 requires this.
112 | def size(*args, &block)
113 | __getobj__.count(*args, &block)
114 | end
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/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 |
11 | class ArrayError < ::StandardError
12 | def self.build(key)
13 | message = "Failed to add #{key.to_s.inspect} property to an array"
14 | new(message)
15 | end
16 | end
17 |
18 | class MergeError < ::StandardError
19 | def self.build(current_value, updates)
20 | message = "Can't merge #{updates.inspect} into #{current_value.inspect}"
21 | new(message)
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/jbuilder/jbuilder.rb:
--------------------------------------------------------------------------------
1 | Jbuilder = Class.new(BasicObject)
2 |
--------------------------------------------------------------------------------
/lib/jbuilder/jbuilder_dependency_tracker.rb:
--------------------------------------------------------------------------------
1 | class Jbuilder::DependencyTracker
2 | EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
3 |
4 | # Matches:
5 | # json.partial! "messages/message"
6 | # json.partial!('messages/message')
7 | #
8 | DIRECT_RENDERS = /
9 | \w+\.partial! # json.partial!
10 | \(?\s* # optional parenthesis
11 | (['"])([^'"]+)\1 # quoted value
12 | /x
13 |
14 | # Matches:
15 | # json.partial! partial: "comments/comment"
16 | # json.comments @post.comments, partial: "comments/comment", as: :comment
17 | # json.array! @posts, partial: "posts/post", as: :post
18 | # = render partial: "account"
19 | #
20 | INDIRECT_RENDERS = /
21 | (?::partial\s*=>|partial:) # partial: or :partial =>
22 | \s* # optional whitespace
23 | (['"])([^'"]+)\1 # quoted value
24 | /x
25 |
26 | def self.call(name, template, view_paths = nil)
27 | new(name, template, view_paths).dependencies
28 | end
29 |
30 | def initialize(name, template, view_paths = nil)
31 | @name, @template, @view_paths = name, template, view_paths
32 | end
33 |
34 | def dependencies
35 | direct_dependencies + indirect_dependencies + explicit_dependencies
36 | end
37 |
38 | private
39 |
40 | attr_reader :name, :template
41 |
42 | def direct_dependencies
43 | source.scan(DIRECT_RENDERS).map(&:second)
44 | end
45 |
46 | def indirect_dependencies
47 | source.scan(INDIRECT_RENDERS).map(&:second)
48 | end
49 |
50 | def explicit_dependencies
51 | dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
52 |
53 | wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("/*") }
54 |
55 | (explicits + resolve_directories(wildcards)).uniq
56 | end
57 |
58 | def resolve_directories(wildcard_dependencies)
59 | return [] unless @view_paths
60 | return [] if wildcard_dependencies.empty?
61 |
62 | # Remove trailing "/*"
63 | prefixes = wildcard_dependencies.map { |query| query[0..-3] }
64 |
65 | @view_paths.flat_map(&:all_template_paths).uniq.filter_map { |path|
66 | path.to_s if prefixes.include?(path.prefix)
67 | }.sort
68 | end
69 |
70 | def source
71 | template.source
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/jbuilder/jbuilder_template.rb:
--------------------------------------------------------------------------------
1 | require 'jbuilder/jbuilder'
2 | require 'jbuilder/collection_renderer'
3 | require 'action_dispatch/http/mime_type'
4 | require 'active_support/cache'
5 |
6 | class JbuilderTemplate < Jbuilder
7 | class << self
8 | attr_accessor :template_lookup_options
9 | end
10 |
11 | self.template_lookup_options = { handlers: [:jbuilder] }
12 |
13 | def initialize(context, *args)
14 | @context = context
15 | @cached_root = nil
16 | super(*args)
17 | end
18 |
19 | # Generates JSON using the template specified with the `:partial` option. For example, the code below will render
20 | # the file `views/comments/_comments.json.jbuilder`, and set a local variable comments with all this message's
21 | # comments, which can be used inside the partial.
22 | #
23 | # Example:
24 | #
25 | # json.partial! 'comments/comments', comments: @message.comments
26 | #
27 | # There are multiple ways to generate a collection of elements as JSON, as ilustrated below:
28 | #
29 | # Example:
30 | #
31 | # json.array! @posts, partial: 'posts/post', as: :post
32 | #
33 | # # or:
34 | # json.partial! 'posts/post', collection: @posts, as: :post
35 | #
36 | # # or:
37 | # json.partial! partial: 'posts/post', collection: @posts, as: :post
38 | #
39 | # # or:
40 | # json.comments @post.comments, partial: 'comments/comment', as: :comment
41 | #
42 | # Aside from that, the `:cached` options is available on Rails >= 6.0. This will cache the rendered results
43 | # effectively using the multi fetch feature.
44 | #
45 | # Example:
46 | #
47 | # json.array! @posts, partial: "posts/post", as: :post, cached: true
48 | #
49 | # json.comments @post.comments, partial: "comments/comment", as: :comment, cached: true
50 | #
51 | def partial!(*args)
52 | if args.one? && _is_active_model?(args.first)
53 | _render_active_model_partial args.first
54 | else
55 | _render_explicit_partial(*args)
56 | end
57 | end
58 |
59 | # Caches the json constructed within the block passed. Has the same signature as the `cache` helper
60 | # method in `ActionView::Helpers::CacheHelper` and so can be used in the same way.
61 | #
62 | # Example:
63 | #
64 | # json.cache! ['v1', @person], expires_in: 10.minutes do
65 | # json.extract! @person, :name, :age
66 | # end
67 | def cache!(key=nil, options={})
68 | if @context.controller.perform_caching
69 | value = _cache_fragment_for(key, options) do
70 | _scope { yield self }
71 | end
72 |
73 | merge! value
74 | else
75 | yield
76 | end
77 | end
78 |
79 | # Caches the json structure at the root using a string rather than the hash structure. This is considerably
80 | # faster, but the drawback is that it only works, as the name hints, at the root. So you cannot
81 | # use this approach to cache deeper inside the hierarchy, like in partials or such. Continue to use #cache! there.
82 | #
83 | # Example:
84 | #
85 | # json.cache_root! @person do
86 | # json.extract! @person, :name, :age
87 | # end
88 | #
89 | # # json.extra 'This will not work either, the root must be exclusive'
90 | def cache_root!(key=nil, options={})
91 | if @context.controller.perform_caching
92 | ::Kernel.raise "cache_root! can't be used after JSON structures have been defined" if @attributes.present?
93 |
94 | @cached_root = _cache_fragment_for([ :root, key ], options) { yield; target! }
95 | else
96 | yield
97 | end
98 | end
99 |
100 | # Conditionally caches the json depending in the condition given as first parameter. Has the same
101 | # signature as the `cache` helper method in `ActionView::Helpers::CacheHelper` and so can be used in
102 | # the same way.
103 | #
104 | # Example:
105 | #
106 | # json.cache_if! !admin?, @person, expires_in: 10.minutes do
107 | # json.extract! @person, :name, :age
108 | # end
109 | def cache_if!(condition, *args, &block)
110 | condition ? cache!(*args, &block) : yield
111 | end
112 |
113 | def target!
114 | @cached_root || super
115 | end
116 |
117 | def array!(collection = [], *args)
118 | options = args.first
119 |
120 | if args.one? && _partial_options?(options)
121 | partial! options.merge(collection: collection)
122 | else
123 | super
124 | end
125 | end
126 |
127 | def set!(name, object = BLANK, *args)
128 | options = args.first
129 |
130 | if args.one? && _partial_options?(options)
131 | _set_inline_partial name, object, options
132 | else
133 | super
134 | end
135 | end
136 |
137 | private
138 |
139 | def _render_partial_with_options(options)
140 | options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached)
141 | options.reverse_merge! ::JbuilderTemplate.template_lookup_options
142 | as = options[:as]
143 |
144 | if as && options.key?(:collection) && CollectionRenderer.supported?
145 | collection = options.delete(:collection) || []
146 | partial = options.delete(:partial)
147 | options[:locals].merge!(json: self)
148 | collection = EnumerableCompat.new(collection) if collection.respond_to?(:count) && !collection.respond_to?(:size)
149 |
150 | if options.has_key?(:layout)
151 | ::Kernel.raise ::NotImplementedError, "The `:layout' option is not supported in collection rendering."
152 | end
153 |
154 | if options.has_key?(:spacer_template)
155 | ::Kernel.raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering."
156 | end
157 |
158 | if collection.present?
159 | results = CollectionRenderer
160 | .new(@context.lookup_context, options) { |&block| _scope(&block) }
161 | .render_collection_with_partial(collection, partial, @context, nil)
162 |
163 | array! if results.respond_to?(:body) && results.body.nil?
164 | else
165 | array!
166 | end
167 | elsif as && options.key?(:collection) && !CollectionRenderer.supported?
168 | # For Rails <= 5.2:
169 | as = as.to_sym
170 | collection = options.delete(:collection)
171 |
172 | if collection.present?
173 | locals = options.delete(:locals)
174 | array! collection do |member|
175 | member_locals = locals.clone
176 | member_locals.merge! collection: collection
177 | member_locals.merge! as => member
178 | _render_partial options.merge(locals: member_locals)
179 | end
180 | else
181 | array!
182 | end
183 | else
184 | _render_partial options
185 | end
186 | end
187 |
188 | def _render_partial(options)
189 | options[:locals].merge! json: self
190 | @context.render options
191 | end
192 |
193 | def _cache_fragment_for(key, options, &block)
194 | key = _cache_key(key, options)
195 | _read_fragment_cache(key, options) || _write_fragment_cache(key, options, &block)
196 | end
197 |
198 | def _read_fragment_cache(key, options = nil)
199 | @context.controller.instrument_fragment_cache :read_fragment, key do
200 | ::Rails.cache.read(key, options)
201 | end
202 | end
203 |
204 | def _write_fragment_cache(key, options = nil)
205 | @context.controller.instrument_fragment_cache :write_fragment, key do
206 | yield.tap do |value|
207 | ::Rails.cache.write(key, value, options)
208 | end
209 | end
210 | end
211 |
212 | def _cache_key(key, options)
213 | name_options = options.slice(:skip_digest, :virtual_path)
214 | key = _fragment_name_with_digest(key, name_options)
215 |
216 | if @context.respond_to?(:combined_fragment_cache_key)
217 | key = @context.combined_fragment_cache_key(key)
218 | else
219 | key = url_for(key).split('://', 2).last if ::Hash === key
220 | end
221 |
222 | ::ActiveSupport::Cache.expand_cache_key(key, :jbuilder)
223 | end
224 |
225 | def _fragment_name_with_digest(key, options)
226 | if @context.respond_to?(:cache_fragment_name)
227 | @context.cache_fragment_name(key, **options)
228 | else
229 | key
230 | end
231 | end
232 |
233 | def _partial_options?(options)
234 | ::Hash === options && options.key?(:as) && options.key?(:partial)
235 | end
236 |
237 | def _is_active_model?(object)
238 | object.class.respond_to?(:model_name) && object.respond_to?(:to_partial_path)
239 | end
240 |
241 | def _set_inline_partial(name, object, options)
242 | value = if object.nil?
243 | []
244 | elsif _is_collection?(object)
245 | _scope{ _render_partial_with_options options.merge(collection: object) }
246 | else
247 | locals = ::Hash[options[:as], object]
248 | _scope{ _render_partial_with_options options.merge(locals: locals) }
249 | end
250 |
251 | set! name, value
252 | end
253 |
254 | def _render_explicit_partial(name_or_options, locals = {})
255 | case name_or_options
256 | when ::Hash
257 | # partial! partial: 'name', foo: 'bar'
258 | options = name_or_options
259 | else
260 | # partial! 'name', locals: {foo: 'bar'}
261 | if locals.one? && (locals.keys.first == :locals)
262 | options = locals.merge(partial: name_or_options)
263 | else
264 | options = { partial: name_or_options, locals: locals }
265 | end
266 | # partial! 'name', foo: 'bar'
267 | as = locals.delete(:as)
268 | options[:as] = as if as.present?
269 | options[:collection] = locals[:collection] if locals.key?(:collection)
270 | end
271 |
272 | _render_partial_with_options options
273 | end
274 |
275 | def _render_active_model_partial(object)
276 | @context.render object, json: self
277 | end
278 | end
279 |
280 | class JbuilderHandler
281 | cattr_accessor :default_format
282 | self.default_format = :json
283 |
284 | def self.call(template, source = nil)
285 | source ||= template.source
286 | # this juggling is required to keep line numbers right in the error
287 | %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source};
288 | json.target! unless (__already_defined && __already_defined != "method")}
289 | end
290 | end
291 |
--------------------------------------------------------------------------------
/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, parameters|
15 | @format[name] = parameters
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 |
--------------------------------------------------------------------------------
/lib/jbuilder/railtie.rb:
--------------------------------------------------------------------------------
1 | require 'rails'
2 | require 'jbuilder/jbuilder_template'
3 |
4 | class Jbuilder
5 | class Railtie < ::Rails::Railtie
6 | initializer :jbuilder do
7 | ActiveSupport.on_load :action_view do
8 | ActionView::Template.register_template_handler :jbuilder, JbuilderHandler
9 | require 'jbuilder/jbuilder_dependency_tracker'
10 | end
11 |
12 | if Rails::VERSION::MAJOR >= 5
13 | module ::ActionController
14 | module ApiRendering
15 | include ActionView::Rendering
16 | end
17 | end
18 |
19 | ActiveSupport.on_load :action_controller do
20 | if name == 'ActionController::API'
21 | include ActionController::Helpers
22 | include ActionController::ImplicitRender
23 | end
24 | end
25 | end
26 | end
27 |
28 | if Rails::VERSION::MAJOR >= 4
29 | generators do |app|
30 | Rails::Generators.configure! app.config.generators
31 | Rails::Generators.hidden_namespaces.uniq!
32 | require 'generators/rails/scaffold_controller_generator'
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/jbuilder/version.rb:
--------------------------------------------------------------------------------
1 | class Jbuilder
2 | VERSION = "2.13.0"
3 | end
4 |
--------------------------------------------------------------------------------
/test/jbuilder_dependency_tracker_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'jbuilder/jbuilder_dependency_tracker'
3 |
4 | class FakeTemplate
5 | attr_reader :source, :handler
6 | def initialize(source, handler = :jbuilder)
7 | @source, @handler = source, handler
8 | end
9 | end
10 |
11 |
12 | class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
13 | def make_tracker(name, source)
14 | template = FakeTemplate.new(source)
15 | Jbuilder::DependencyTracker.new(name, template)
16 | end
17 |
18 | def track_dependencies(source)
19 | make_tracker('jbuilder_template', source).dependencies
20 | end
21 |
22 | test 'detects dependency via direct partial! call' do
23 | dependencies = track_dependencies <<-RUBY
24 | json.partial! 'path/to/partial', foo: bar
25 | json.partial! 'path/to/another/partial', :fizz => buzz
26 | RUBY
27 |
28 | assert_equal %w[path/to/partial path/to/another/partial], dependencies
29 | end
30 |
31 | test 'detects dependency via direct partial! call with parens' do
32 | dependencies = track_dependencies <<-RUBY
33 | json.partial!("path/to/partial")
34 | RUBY
35 |
36 | assert_equal %w[path/to/partial], dependencies
37 | end
38 |
39 | test 'detects partial with options (1.9 style)' do
40 | dependencies = track_dependencies <<-RUBY
41 | json.partial! hello: 'world', partial: 'path/to/partial', foo: :bar
42 | RUBY
43 |
44 | assert_equal %w[path/to/partial], dependencies
45 | end
46 |
47 | test 'detects partial with options (1.8 style)' do
48 | dependencies = track_dependencies <<-RUBY
49 | json.partial! :hello => 'world', :partial => 'path/to/partial', :foo => :bar
50 | RUBY
51 |
52 | assert_equal %w[path/to/partial], dependencies
53 | end
54 |
55 | test 'detects partial in indirect collection calls' do
56 | dependencies = track_dependencies <<-RUBY
57 | json.comments @post.comments, partial: 'comments/comment', as: :comment
58 | RUBY
59 |
60 | assert_equal %w[comments/comment], dependencies
61 | end
62 |
63 | test 'detects explicit dependency' do
64 | dependencies = track_dependencies <<-RUBY
65 | # Template Dependency: path/to/partial
66 | json.foo 'bar'
67 | RUBY
68 |
69 | assert_equal %w[path/to/partial], dependencies
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/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 | assert_file "app/views/posts/_post.json.jbuilder"
18 | end
19 |
20 | test 'index content' do
21 | run_generator
22 |
23 | assert_file 'app/views/posts/index.json.jbuilder' do |content|
24 | assert_match %r{json\.array! @posts, partial: "posts/post", as: :post}, content
25 | end
26 |
27 | assert_file 'app/views/posts/show.json.jbuilder' do |content|
28 | assert_match %r{json\.partial! "posts/post", post: @post}, content
29 | end
30 |
31 | assert_file 'app/views/posts/_post.json.jbuilder' do |content|
32 | assert_match %r{json\.extract! post, :id, :title, :body}, content
33 | assert_match %r{:created_at, :updated_at}, content
34 | assert_match %r{json\.url post_url\(post, format: :json\)}, content
35 | end
36 | end
37 |
38 | test 'timestamps are not generated in partial with --no-timestamps' do
39 | run_generator %w(Post title body:text --no-timestamps)
40 |
41 | assert_file 'app/views/posts/_post.json.jbuilder' do |content|
42 | assert_match %r{json\.extract! post, :id, :title, :body$}, content
43 | assert_no_match %r{:created_at, :updated_at}, content
44 | end
45 | end
46 |
47 | test 'namespaced views are generated correctly for index' do
48 | run_generator %w(Admin::Post --model-name=Post)
49 |
50 | assert_file 'app/views/admin/posts/index.json.jbuilder' do |content|
51 | assert_match %r{json\.array! @posts, partial: "admin/posts/post", as: :post}, content
52 | end
53 |
54 | assert_file 'app/views/admin/posts/show.json.jbuilder' do |content|
55 | assert_match %r{json\.partial! "admin/posts/post", post: @post}, content
56 | end
57 | end
58 |
59 | if Rails::VERSION::MAJOR >= 6
60 | test 'handles virtual attributes' do
61 | run_generator %w(Message content:rich_text video:attachment photos:attachments)
62 |
63 | assert_file 'app/views/messages/_message.json.jbuilder' do |content|
64 | assert_match %r{json\.content message\.content\.to_s}, content
65 | assert_match %r{json\.video url_for\(message\.video\)}, content
66 | assert_match %r{json\.photos do\n json\.array!\(message\.photos\) do \|photo\|\n json\.id photo\.id\n json\.url url_for\(photo\)\n end\nend}, content
67 | end
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/test/jbuilder_template_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "action_view/testing/resolvers"
3 |
4 | class JbuilderTemplateTest < ActiveSupport::TestCase
5 | POST_PARTIAL = <<-JBUILDER
6 | json.extract! post, :id, :body
7 | json.author do
8 | first_name, last_name = post.author_name.split(nil, 2)
9 | json.first_name first_name
10 | json.last_name last_name
11 | end
12 | JBUILDER
13 |
14 | COLLECTION_PARTIAL = <<-JBUILDER
15 | json.extract! collection, :id, :name
16 | JBUILDER
17 |
18 | RACER_PARTIAL = <<-JBUILDER
19 | json.extract! racer, :id, :name
20 | JBUILDER
21 |
22 | PARTIALS = {
23 | "_partial.json.jbuilder" => "json.content content",
24 | "_post.json.jbuilder" => POST_PARTIAL,
25 | "racers/_racer.json.jbuilder" => RACER_PARTIAL,
26 | "_collection.json.jbuilder" => COLLECTION_PARTIAL,
27 |
28 | # Ensure we find only Jbuilder partials from within Jbuilder templates.
29 | "_post.html.erb" => "Hello world!"
30 | }
31 |
32 | AUTHORS = [ "David Heinemeier Hansson", "Pavel Pravosud" ].cycle
33 | POSTS = (1..10).collect { |i| Post.new(i, "Post ##{i}", AUTHORS.next) }
34 |
35 | setup { Rails.cache.clear }
36 |
37 | test "basic template" do
38 | result = render('json.content "hello"')
39 | assert_equal "hello", result["content"]
40 | end
41 |
42 | test "partial by name with top-level locals" do
43 | result = render('json.partial! "partial", content: "hello"')
44 | assert_equal "hello", result["content"]
45 | end
46 |
47 | test "partial by name with nested locals" do
48 | result = render('json.partial! "partial", locals: { content: "hello" }')
49 | assert_equal "hello", result["content"]
50 | end
51 |
52 | test "partial by name with hash value omission (punning) as last statement [3.1+]" do
53 | major, minor, _ = RUBY_VERSION.split(".").map(&:to_i)
54 | return unless (major == 3 && minor >= 1) || major > 3
55 |
56 | result = render(<<-JBUILDER)
57 | content = "hello"
58 | json.partial! "partial", content:
59 | JBUILDER
60 | assert_equal "hello", result["content"]
61 | end
62 |
63 | test "partial by options containing nested locals" do
64 | result = render('json.partial! partial: "partial", locals: { content: "hello" }')
65 | assert_equal "hello", result["content"]
66 | end
67 |
68 | test "partial by options containing top-level locals" do
69 | result = render('json.partial! partial: "partial", content: "hello"')
70 | assert_equal "hello", result["content"]
71 | end
72 |
73 | test "partial for Active Model" do
74 | result = render('json.partial! @racer', racer: Racer.new(123, "Chris Harris"))
75 | assert_equal 123, result["id"]
76 | assert_equal "Chris Harris", result["name"]
77 | end
78 |
79 | test "partial collection by name with symbol local" do
80 | result = render('json.partial! "post", collection: @posts, as: :post', posts: POSTS)
81 | assert_equal 10, result.count
82 | assert_equal "Post #5", result[4]["body"]
83 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
84 | assert_equal "Pavel", result[5]["author"]["first_name"]
85 | end
86 |
87 | test "partial collection by name with caching" do
88 | result = render('json.partial! "post", collection: @posts, cached: true, as: :post', posts: POSTS)
89 | assert_equal 10, result.count
90 | assert_equal "Post #5", result[4]["body"]
91 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
92 | assert_equal "Pavel", result[5]["author"]["first_name"]
93 | end
94 |
95 | test "partial collection by name with string local" do
96 | result = render('json.partial! "post", collection: @posts, as: "post"', posts: POSTS)
97 | assert_equal 10, result.count
98 | assert_equal "Post #5", result[4]["body"]
99 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
100 | assert_equal "Pavel", result[5]["author"]["first_name"]
101 | end
102 |
103 | test "partial collection by options" do
104 | result = render('json.partial! partial: "post", collection: @posts, as: :post', posts: POSTS)
105 | assert_equal 10, result.count
106 | assert_equal "Post #5", result[4]["body"]
107 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
108 | assert_equal "Pavel", result[5]["author"]["first_name"]
109 | end
110 |
111 | test "nil partial collection by name" do
112 | Jbuilder::CollectionRenderer.expects(:new).never
113 | assert_equal [], render('json.partial! "post", collection: @posts, as: :post', posts: nil)
114 | end
115 |
116 | test "nil partial collection by options" do
117 | Jbuilder::CollectionRenderer.expects(:new).never
118 | assert_equal [], render('json.partial! partial: "post", collection: @posts, as: :post', posts: nil)
119 | end
120 |
121 | test "array of partials" do
122 | result = render('json.array! @posts, partial: "post", as: :post', posts: POSTS)
123 | assert_equal 10, result.count
124 | assert_equal "Post #5", result[4]["body"]
125 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
126 | assert_equal "Pavel", result[5]["author"]["first_name"]
127 | end
128 |
129 | test "empty array of partials from empty collection" do
130 | Jbuilder::CollectionRenderer.expects(:new).never
131 | assert_equal [], render('json.array! @posts, partial: "post", as: :post', posts: [])
132 | end
133 |
134 | test "empty array of partials from nil collection" do
135 | Jbuilder::CollectionRenderer.expects(:new).never
136 | assert_equal [], render('json.array! @posts, partial: "post", as: :post', posts: nil)
137 | end
138 |
139 | test "array of partials under key" do
140 | result = render('json.posts @posts, partial: "post", as: :post', posts: POSTS)
141 | assert_equal 10, result["posts"].count
142 | assert_equal "Post #5", result["posts"][4]["body"]
143 | assert_equal "Heinemeier Hansson", result["posts"][2]["author"]["last_name"]
144 | assert_equal "Pavel", result["posts"][5]["author"]["first_name"]
145 | end
146 |
147 | test "empty array of partials under key from nil collection" do
148 | Jbuilder::CollectionRenderer.expects(:new).never
149 | result = render('json.posts @posts, partial: "post", as: :post', posts: nil)
150 | assert_equal [], result["posts"]
151 | end
152 |
153 | test "empty array of partials under key from an empy collection" do
154 | Jbuilder::CollectionRenderer.expects(:new).never
155 | result = render('json.posts @posts, partial: "post", as: :post', posts: [])
156 | assert_equal [], result["posts"]
157 | end
158 |
159 | test "object fragment caching" do
160 | render(<<-JBUILDER)
161 | json.cache! "cache-key" do
162 | json.name "Hit"
163 | end
164 | JBUILDER
165 |
166 | hit = render('json.cache! "cache-key" do; end')
167 | assert_equal "Hit", hit["name"]
168 | end
169 |
170 | test "conditional object fragment caching" do
171 | render(<<-JBUILDER)
172 | json.cache_if! true, "cache-key" do
173 | json.a "Hit"
174 | end
175 |
176 | json.cache_if! false, "cache-key" do
177 | json.b "Hit"
178 | end
179 | JBUILDER
180 |
181 | result = render(<<-JBUILDER)
182 | json.cache_if! true, "cache-key" do
183 | json.a "Miss"
184 | end
185 |
186 | json.cache_if! false, "cache-key" do
187 | json.b "Miss"
188 | end
189 | JBUILDER
190 |
191 | assert_equal "Hit", result["a"]
192 | assert_equal "Miss", result["b"]
193 | end
194 |
195 | test "object fragment caching with expiry" do
196 | travel_to Time.iso8601("2018-05-12T11:29:00-04:00")
197 |
198 | render <<-JBUILDER
199 | json.cache! "cache-key", expires_in: 1.minute do
200 | json.name "Hit"
201 | end
202 | JBUILDER
203 |
204 | travel 30.seconds
205 |
206 | result = render(<<-JBUILDER)
207 | json.cache! "cache-key", expires_in: 1.minute do
208 | json.name "Miss"
209 | end
210 | JBUILDER
211 |
212 | assert_equal "Hit", result["name"]
213 |
214 | travel 31.seconds
215 |
216 | result = render(<<-JBUILDER)
217 | json.cache! "cache-key", expires_in: 1.minute do
218 | json.name "Miss"
219 | end
220 | JBUILDER
221 |
222 | assert_equal "Miss", result["name"]
223 | end
224 |
225 | test "object root caching" do
226 | render <<-JBUILDER
227 | json.cache_root! "cache-key" do
228 | json.name "Hit"
229 | end
230 | JBUILDER
231 |
232 | assert_equal JSON.dump(name: "Hit"), Rails.cache.read("jbuilder/root/cache-key")
233 |
234 | result = render(<<-JBUILDER)
235 | json.cache_root! "cache-key" do
236 | json.name "Miss"
237 | end
238 | JBUILDER
239 |
240 | assert_equal "Hit", result["name"]
241 | end
242 |
243 | test "array fragment caching" do
244 | render <<-JBUILDER
245 | json.cache! "cache-key" do
246 | json.array! %w[ a b c ]
247 | end
248 | JBUILDER
249 |
250 | assert_equal %w[ a b c ], render('json.cache! "cache-key" do; end')
251 | end
252 |
253 | test "array root caching" do
254 | render <<-JBUILDER
255 | json.cache_root! "cache-key" do
256 | json.array! %w[ a b c ]
257 | end
258 | JBUILDER
259 |
260 | assert_equal JSON.dump(%w[ a b c ]), Rails.cache.read("jbuilder/root/cache-key")
261 |
262 | assert_equal %w[ a b c ], render(<<-JBUILDER)
263 | json.cache_root! "cache-key" do
264 | json.array! %w[ d e f ]
265 | end
266 | JBUILDER
267 | end
268 |
269 | test "failing to cache root after JSON structures have been defined" do
270 | assert_raises ActionView::Template::Error, "cache_root! can't be used after JSON structures have been defined" do
271 | render <<-JBUILDER
272 | json.name "Kaboom"
273 | json.cache_root! "cache-key" do
274 | json.name "Miss"
275 | end
276 | JBUILDER
277 | end
278 | end
279 |
280 | test "empty fragment caching" do
281 | render 'json.cache! "nothing" do; end'
282 |
283 | result = nil
284 |
285 | assert_nothing_raised do
286 | result = render(<<-JBUILDER)
287 | json.foo "bar"
288 | json.cache! "nothing" do; end
289 | JBUILDER
290 | end
291 |
292 | assert_equal "bar", result["foo"]
293 | end
294 |
295 | test "cache instrumentation" do
296 | payloads = {}
297 |
298 | ActiveSupport::Notifications.subscribe("read_fragment.action_controller") { |*args| payloads[:read] = args.last }
299 | ActiveSupport::Notifications.subscribe("write_fragment.action_controller") { |*args| payloads[:write] = args.last }
300 |
301 | render <<-JBUILDER
302 | json.cache! "cache-key" do
303 | json.name "Cache"
304 | end
305 | JBUILDER
306 |
307 | assert_equal "jbuilder/cache-key", payloads[:read][:key]
308 | assert_equal "jbuilder/cache-key", payloads[:write][:key]
309 | end
310 |
311 | test "camelized keys" do
312 | result = render(<<-JBUILDER)
313 | json.key_format! camelize: [:lower]
314 | json.first_name "David"
315 | JBUILDER
316 |
317 | assert_equal "David", result["firstName"]
318 | end
319 |
320 | if JbuilderTemplate::CollectionRenderer.supported?
321 | test "returns an empty array for an empty collection" do
322 | Jbuilder::CollectionRenderer.expects(:new).never
323 | result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: [])
324 |
325 | # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array.
326 | assert_equal [], result
327 | end
328 |
329 | test "works with an enumerable object" do
330 | enumerable_class = Class.new do
331 | include Enumerable
332 |
333 | def each(&block)
334 | [].each(&block)
335 | end
336 | end
337 |
338 | result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: enumerable_class.new)
339 |
340 | # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array.
341 | assert_equal [], result
342 | end
343 |
344 | test "supports the cached: true option" do
345 | result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS)
346 |
347 | assert_equal 10, result.count
348 | assert_equal "Post #5", result[4]["body"]
349 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
350 | assert_equal "Pavel", result[5]["author"]["first_name"]
351 |
352 | expected = {
353 | "id" => 1,
354 | "body" => "Post #1",
355 | "author" => {
356 | "first_name" => "David",
357 | "last_name" => "Heinemeier Hansson"
358 | }
359 | }
360 |
361 | assert_equal expected, Rails.cache.read("post-1")
362 |
363 | result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS)
364 |
365 | assert_equal 10, result.count
366 | assert_equal "Post #5", result[4]["body"]
367 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
368 | assert_equal "Pavel", result[5]["author"]["first_name"]
369 | end
370 |
371 | test "supports the cached: ->() {} option" do
372 | result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS)
373 |
374 | assert_equal 10, result.count
375 | assert_equal "Post #5", result[4]["body"]
376 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
377 | assert_equal "Pavel", result[5]["author"]["first_name"]
378 |
379 | expected = {
380 | "id" => 1,
381 | "body" => "Post #1",
382 | "author" => {
383 | "first_name" => "David",
384 | "last_name" => "Heinemeier Hansson"
385 | }
386 | }
387 |
388 | assert_equal expected, Rails.cache.read("post-1/foo")
389 |
390 | result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS)
391 |
392 | assert_equal 10, result.count
393 | assert_equal "Post #5", result[4]["body"]
394 | assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"]
395 | assert_equal "Pavel", result[5]["author"]["first_name"]
396 | end
397 |
398 | test "raises an error on a render call with the :layout option" do
399 | error = assert_raises NotImplementedError do
400 | render('json.array! @posts, partial: "post", as: :post, layout: "layout"', posts: POSTS)
401 | end
402 |
403 | assert_equal "The `:layout' option is not supported in collection rendering.", error.message
404 | end
405 |
406 | test "raises an error on a render call with the :spacer_template option" do
407 | error = assert_raises NotImplementedError do
408 | render('json.array! @posts, partial: "post", as: :post, spacer_template: "template"', posts: POSTS)
409 | end
410 |
411 | assert_equal "The `:spacer_template' option is not supported in collection rendering.", error.message
412 | end
413 | end
414 |
415 | private
416 | def render(*args)
417 | JSON.load render_without_parsing(*args)
418 | end
419 |
420 | def render_without_parsing(source, assigns = {})
421 | view = build_view(fixtures: PARTIALS.merge("source.json.jbuilder" => source), assigns: assigns)
422 | view.render(template: "source")
423 | end
424 |
425 | def build_view(options = {})
426 | resolver = ActionView::FixtureResolver.new(options.fetch(:fixtures))
427 | lookup_context = ActionView::LookupContext.new([ resolver ], {}, [""])
428 | controller = ActionView::TestCase::TestController.new
429 |
430 | # TODO: Use with_empty_template_cache unconditionally after dropping support for Rails <6.0.
431 | view = if ActionView::Base.respond_to?(:with_empty_template_cache)
432 | ActionView::Base.with_empty_template_cache.new(lookup_context, options.fetch(:assigns, {}), controller)
433 | else
434 | ActionView::Base.new(lookup_context, options.fetch(:assigns, {}), controller)
435 | end
436 |
437 | def view.view_cache_dependencies; []; end
438 | def view.combined_fragment_cache_key(key) [ key ] end
439 | def view.cache_fragment_name(key, *) key end
440 | def view.fragment_name_with_digest(key) key end
441 |
442 | view
443 | end
444 | end
445 |
--------------------------------------------------------------------------------
/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 | delegate :map, :count, to: :@collection
17 | end
18 |
19 | class VeryBasicWrapper < BasicObject
20 | def initialize(thing)
21 | @thing = thing
22 | end
23 |
24 | def method_missing(name, *args, &block)
25 | @thing.send name, *args, &block
26 | end
27 | end
28 |
29 | # This is not Struct, because structs are Enumerable
30 | class Person
31 | attr_reader :name, :age
32 |
33 | def initialize(name, age)
34 | @name, @age = name, age
35 | end
36 | end
37 |
38 | class RelationMock
39 | include Enumerable
40 |
41 | def each(&block)
42 | [Person.new('Bob', 30), Person.new('Frank', 50)].each(&block)
43 | end
44 |
45 | def empty?
46 | false
47 | end
48 | end
49 |
50 |
51 | class JbuilderTest < ActiveSupport::TestCase
52 | teardown do
53 | Jbuilder.send :class_variable_set, '@@key_formatter', nil
54 | end
55 |
56 | test 'single key' do
57 | result = jbuild do |json|
58 | json.content 'hello'
59 | end
60 |
61 | assert_equal 'hello', result['content']
62 | end
63 |
64 | test 'single key with false value' do
65 | result = jbuild do |json|
66 | json.content false
67 | end
68 |
69 | assert_equal false, result['content']
70 | end
71 |
72 | test 'single key with nil value' do
73 | result = jbuild do |json|
74 | json.content nil
75 | end
76 |
77 | assert result.has_key?('content')
78 | assert_nil result['content']
79 | end
80 |
81 | test 'multiple keys' do
82 | result = jbuild do |json|
83 | json.title 'hello'
84 | json.content 'world'
85 | end
86 |
87 | assert_equal 'hello', result['title']
88 | assert_equal 'world', result['content']
89 | end
90 |
91 | test 'extracting from object' do
92 | person = Struct.new(:name, :age).new('David', 32)
93 |
94 | result = jbuild do |json|
95 | json.extract! person, :name, :age
96 | end
97 |
98 | assert_equal 'David', result['name']
99 | assert_equal 32, result['age']
100 | end
101 |
102 | test 'extracting from object using call style' do
103 | person = Struct.new(:name, :age).new('David', 32)
104 |
105 | result = jbuild do |json|
106 | json.(person, :name, :age)
107 | end
108 |
109 | assert_equal 'David', result['name']
110 | assert_equal 32, result['age']
111 | end
112 |
113 | test 'extracting from hash' do
114 | person = {:name => 'Jim', :age => 34}
115 |
116 | result = jbuild do |json|
117 | json.extract! person, :name, :age
118 | end
119 |
120 | assert_equal 'Jim', result['name']
121 | assert_equal 34, result['age']
122 | end
123 |
124 | test 'nesting single child with block' do
125 | result = jbuild do |json|
126 | json.author do
127 | json.name 'David'
128 | json.age 32
129 | end
130 | end
131 |
132 | assert_equal 'David', result['author']['name']
133 | assert_equal 32, result['author']['age']
134 | end
135 |
136 | test 'empty block handling' do
137 | result = jbuild do |json|
138 | json.foo 'bar'
139 | json.author do
140 | end
141 | end
142 |
143 | assert_equal 'bar', result['foo']
144 | assert !result.key?('author')
145 | end
146 |
147 | test 'blocks are additive' do
148 | result = jbuild do |json|
149 | json.author do
150 | json.name 'David'
151 | end
152 |
153 | json.author do
154 | json.age 32
155 | end
156 | end
157 |
158 | assert_equal 'David', result['author']['name']
159 | assert_equal 32, result['author']['age']
160 | end
161 |
162 | test 'nested blocks are additive' do
163 | result = jbuild do |json|
164 | json.author do
165 | json.name do
166 | json.first 'David'
167 | end
168 | end
169 |
170 | json.author do
171 | json.name do
172 | json.last 'Heinemeier Hansson'
173 | end
174 | end
175 | end
176 |
177 | assert_equal 'David', result['author']['name']['first']
178 | assert_equal 'Heinemeier Hansson', result['author']['name']['last']
179 | end
180 |
181 | test 'support merge! method' do
182 | result = jbuild do |json|
183 | json.merge! 'foo' => 'bar'
184 | end
185 |
186 | assert_equal 'bar', result['foo']
187 | end
188 |
189 | test 'support merge! method in a block' do
190 | result = jbuild do |json|
191 | json.author do
192 | json.merge! 'name' => 'Pavel'
193 | end
194 | end
195 |
196 | assert_equal 'Pavel', result['author']['name']
197 | end
198 |
199 | test 'support merge! method with Jbuilder instance' do
200 | obj = jbuild do |json|
201 | json.foo 'bar'
202 | end
203 |
204 | result = jbuild do |json|
205 | json.merge! obj
206 | end
207 |
208 | assert_equal 'bar', result['foo']
209 | end
210 |
211 | test 'blocks are additive via extract syntax' do
212 | person = Person.new('Pavel', 27)
213 |
214 | result = jbuild do |json|
215 | json.author person, :age
216 | json.author person, :name
217 | end
218 |
219 | assert_equal 'Pavel', result['author']['name']
220 | assert_equal 27, result['author']['age']
221 | end
222 |
223 | test 'arrays are additive' do
224 | result = jbuild do |json|
225 | json.array! %w[foo]
226 | json.array! %w[bar]
227 | end
228 |
229 | assert_equal %w[foo bar], result
230 | end
231 |
232 | test 'nesting multiple children with block' do
233 | result = jbuild do |json|
234 | json.comments do
235 | json.child! { json.content 'hello' }
236 | json.child! { json.content 'world' }
237 | end
238 | end
239 |
240 | assert_equal 'hello', result['comments'].first['content']
241 | assert_equal 'world', result['comments'].second['content']
242 | end
243 |
244 | test 'nesting single child with inline extract' do
245 | person = Person.new('David', 32)
246 |
247 | result = jbuild do |json|
248 | json.author person, :name, :age
249 | end
250 |
251 | assert_equal 'David', result['author']['name']
252 | assert_equal 32, result['author']['age']
253 | end
254 |
255 | test 'nesting multiple children from array' do
256 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ]
257 |
258 | result = jbuild do |json|
259 | json.comments comments, :content
260 | end
261 |
262 | assert_equal ['content'], result['comments'].first.keys
263 | assert_equal 'hello', result['comments'].first['content']
264 | assert_equal 'world', result['comments'].second['content']
265 | end
266 |
267 | test 'nesting multiple children from array when child array is empty' do
268 | comments = []
269 |
270 | result = jbuild do |json|
271 | json.name 'Parent'
272 | json.comments comments, :content
273 | end
274 |
275 | assert_equal 'Parent', result['name']
276 | assert_equal [], result['comments']
277 | end
278 |
279 | test 'nesting multiple children from array with inline loop' do
280 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ]
281 |
282 | result = jbuild do |json|
283 | json.comments comments do |comment|
284 | json.content comment.content
285 | end
286 | end
287 |
288 | assert_equal ['content'], result['comments'].first.keys
289 | assert_equal 'hello', result['comments'].first['content']
290 | assert_equal 'world', result['comments'].second['content']
291 | end
292 |
293 | test 'handles nil-collections as empty arrays' do
294 | result = jbuild do |json|
295 | json.comments nil do |comment|
296 | json.content comment.content
297 | end
298 | end
299 |
300 | assert_equal [], result['comments']
301 | end
302 |
303 | test 'nesting multiple children from a non-Enumerable that responds to #map' do
304 | comments = NonEnumerable.new([ Comment.new('hello', 1), Comment.new('world', 2) ])
305 |
306 | result = jbuild do |json|
307 | json.comments comments, :content
308 | end
309 |
310 | assert_equal ['content'], result['comments'].first.keys
311 | assert_equal 'hello', result['comments'].first['content']
312 | assert_equal 'world', result['comments'].second['content']
313 | end
314 |
315 | test 'nesting multiple children from a non-Enumerable that responds to #map with inline loop' do
316 | comments = NonEnumerable.new([ Comment.new('hello', 1), Comment.new('world', 2) ])
317 |
318 | result = jbuild do |json|
319 | json.comments comments do |comment|
320 | json.content comment.content
321 | end
322 | end
323 |
324 | assert_equal ['content'], result['comments'].first.keys
325 | assert_equal 'hello', result['comments'].first['content']
326 | assert_equal 'world', result['comments'].second['content']
327 | end
328 |
329 | test 'array! casts array-like objects to array before merging' do
330 | wrapped_array = VeryBasicWrapper.new(%w[foo bar])
331 |
332 | result = jbuild do |json|
333 | json.array! wrapped_array
334 | end
335 |
336 | assert_equal %w[foo bar], result
337 | end
338 |
339 | test 'nesting multiple children from array with inline loop on root' do
340 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ]
341 |
342 | result = jbuild do |json|
343 | json.call(comments) do |comment|
344 | json.content comment.content
345 | end
346 | end
347 |
348 | assert_equal 'hello', result.first['content']
349 | assert_equal 'world', result.second['content']
350 | end
351 |
352 | test 'array nested inside nested hash' do
353 | result = jbuild do |json|
354 | json.author do
355 | json.name 'David'
356 | json.age 32
357 |
358 | json.comments do
359 | json.child! { json.content 'hello' }
360 | json.child! { json.content 'world' }
361 | end
362 | end
363 | end
364 |
365 | assert_equal 'hello', result['author']['comments'].first['content']
366 | assert_equal 'world', result['author']['comments'].second['content']
367 | end
368 |
369 | test 'array nested inside array' do
370 | result = jbuild do |json|
371 | json.comments do
372 | json.child! do
373 | json.authors do
374 | json.child! do
375 | json.name 'david'
376 | end
377 | end
378 | end
379 | end
380 | end
381 |
382 | assert_equal 'david', result['comments'].first['authors'].first['name']
383 | end
384 |
385 | test 'directly set an array nested in another array' do
386 | data = [ { :department => 'QA', :not_in_json => 'hello', :names => ['John', 'David'] } ]
387 |
388 | result = jbuild do |json|
389 | json.array! data do |object|
390 | json.department object[:department]
391 | json.names do
392 | json.array! object[:names]
393 | end
394 | end
395 | end
396 |
397 | assert_equal 'David', result[0]['names'].last
398 | assert !result[0].key?('not_in_json')
399 | end
400 |
401 | test 'nested jbuilder objects' do
402 | to_nest = Jbuilder.new{ |json| json.nested_value 'Nested Test' }
403 |
404 | result = jbuild do |json|
405 | json.value 'Test'
406 | json.nested to_nest
407 | end
408 |
409 | expected = {'value' => 'Test', 'nested' => {'nested_value' => 'Nested Test'}}
410 | assert_equal expected, result
411 | end
412 |
413 | test 'nested jbuilder object via set!' do
414 | to_nest = Jbuilder.new{ |json| json.nested_value 'Nested Test' }
415 |
416 | result = jbuild do |json|
417 | json.value 'Test'
418 | json.set! :nested, to_nest
419 | end
420 |
421 | expected = {'value' => 'Test', 'nested' => {'nested_value' => 'Nested Test'}}
422 | assert_equal expected, result
423 | end
424 |
425 | test 'top-level array' do
426 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ]
427 |
428 | result = jbuild do |json|
429 | json.array! comments do |comment|
430 | json.content comment.content
431 | end
432 | end
433 |
434 | assert_equal 'hello', result.first['content']
435 | assert_equal 'world', result.second['content']
436 | end
437 |
438 | test 'it allows using next in array block to skip value' do
439 | comments = [ Comment.new('hello', 1), Comment.new('skip', 2), Comment.new('world', 3) ]
440 | result = jbuild do |json|
441 | json.array! comments do |comment|
442 | next if comment.id == 2
443 | json.content comment.content
444 | end
445 | end
446 |
447 | assert_equal 2, result.length
448 | assert_equal 'hello', result.first['content']
449 | assert_equal 'world', result.second['content']
450 | end
451 |
452 | test 'extract attributes directly from array' do
453 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ]
454 |
455 | result = jbuild do |json|
456 | json.array! comments, :content, :id
457 | end
458 |
459 | assert_equal 'hello', result.first['content']
460 | assert_equal 1, result.first['id']
461 | assert_equal 'world', result.second['content']
462 | assert_equal 2, result.second['id']
463 | end
464 |
465 | test 'empty top-level array' do
466 | comments = []
467 |
468 | result = jbuild do |json|
469 | json.array! comments do |comment|
470 | json.content comment.content
471 | end
472 | end
473 |
474 | assert_equal [], result
475 | end
476 |
477 | test 'dynamically set a key/value' do
478 | result = jbuild do |json|
479 | json.set! :each, 'stuff'
480 | end
481 |
482 | assert_equal 'stuff', result['each']
483 | end
484 |
485 | test 'dynamically set a key/nested child with block' do
486 | result = jbuild do |json|
487 | json.set! :author do
488 | json.name 'David'
489 | json.age 32
490 | end
491 | end
492 |
493 | assert_equal 'David', result['author']['name']
494 | assert_equal 32, result['author']['age']
495 | end
496 |
497 | test 'dynamically sets a collection' do
498 | comments = [ Comment.new('hello', 1), Comment.new('world', 2) ]
499 |
500 | result = jbuild do |json|
501 | json.set! :comments, comments, :content
502 | end
503 |
504 | assert_equal ['content'], result['comments'].first.keys
505 | assert_equal 'hello', result['comments'].first['content']
506 | assert_equal 'world', result['comments'].second['content']
507 | end
508 |
509 | test 'query like object' do
510 | result = jbuild do |json|
511 | json.relations RelationMock.new, :name, :age
512 | end
513 |
514 | assert_equal 2, result['relations'].length
515 | assert_equal 'Bob', result['relations'][0]['name']
516 | assert_equal 50, result['relations'][1]['age']
517 | end
518 |
519 | test 'initialize via options hash' do
520 | jbuilder = Jbuilder.new(key_formatter: 1, ignore_nil: 2)
521 | assert_equal 1, jbuilder.instance_eval{ @key_formatter }
522 | assert_equal 2, jbuilder.instance_eval{ @ignore_nil }
523 | end
524 |
525 | test 'key_format! with parameter' do
526 | result = jbuild do |json|
527 | json.key_format! camelize: [:lower]
528 | json.camel_style 'for JS'
529 | end
530 |
531 | assert_equal ['camelStyle'], result.keys
532 | end
533 |
534 | test 'key_format! with parameter not as an array' do
535 | result = jbuild do |json|
536 | json.key_format! :camelize => :lower
537 | json.camel_style 'for JS'
538 | end
539 |
540 | assert_equal ['camelStyle'], result.keys
541 | end
542 |
543 | test 'key_format! propagates to child elements' do
544 | result = jbuild do |json|
545 | json.key_format! :upcase
546 | json.level1 'one'
547 | json.level2 do
548 | json.value 'two'
549 | end
550 | end
551 |
552 | assert_equal 'one', result['LEVEL1']
553 | assert_equal 'two', result['LEVEL2']['VALUE']
554 | end
555 |
556 | test 'key_format! resets after child element' do
557 | result = jbuild do |json|
558 | json.level2 do
559 | json.key_format! :upcase
560 | json.value 'two'
561 | end
562 | json.level1 'one'
563 | end
564 |
565 | assert_equal 'two', result['level2']['VALUE']
566 | assert_equal 'one', result['level1']
567 | end
568 |
569 | test 'key_format! can be changed in child elements' do
570 | result = jbuild do |json|
571 | json.key_format! camelize: :lower
572 |
573 | json.level_one do
574 | json.key_format! :upcase
575 | json.value 'two'
576 | end
577 | end
578 |
579 | assert_equal ['levelOne'], result.keys
580 | assert_equal ['VALUE'], result['levelOne'].keys
581 | end
582 |
583 | test 'key_format! can be changed in array!' do
584 | result = jbuild do |json|
585 | json.key_format! camelize: :lower
586 |
587 | json.level_one do
588 | json.array! [{value: 'two'}] do |object|
589 | json.key_format! :upcase
590 | json.value object[:value]
591 | end
592 | end
593 | end
594 |
595 | assert_equal ['levelOne'], result.keys
596 | assert_equal ['VALUE'], result['levelOne'][0].keys
597 | end
598 |
599 | test 'key_format! with no parameter' do
600 | result = jbuild do |json|
601 | json.key_format! :upcase
602 | json.lower 'Value'
603 | end
604 |
605 | assert_equal ['LOWER'], result.keys
606 | end
607 |
608 | test 'key_format! with multiple steps' do
609 | result = jbuild do |json|
610 | json.key_format! :upcase, :pluralize
611 | json.pill 'foo'
612 | end
613 |
614 | assert_equal ['PILLs'], result.keys
615 | end
616 |
617 | test 'key_format! with lambda/proc' do
618 | result = jbuild do |json|
619 | json.key_format! ->(key){ key + ' and friends' }
620 | json.oats 'foo'
621 | end
622 |
623 | assert_equal ['oats and friends'], result.keys
624 | end
625 |
626 | test 'key_format! is not applied deeply by default' do
627 | names = { first_name: 'camel', last_name: 'case' }
628 | result = jbuild do |json|
629 | json.key_format! camelize: :lower
630 | json.set! :all_names, names
631 | end
632 |
633 | assert_equal %i[first_name last_name], result['allNames'].keys
634 | end
635 |
636 | test 'applying key_format! deeply can be enabled per scope' do
637 | names = { first_name: 'camel', last_name: 'case' }
638 | result = jbuild do |json|
639 | json.key_format! camelize: :lower
640 | json.scope do
641 | json.deep_format_keys!
642 | json.set! :all_names, names
643 | end
644 | json.set! :all_names, names
645 | end
646 |
647 | assert_equal %w[firstName lastName], result['scope']['allNames'].keys
648 | assert_equal %i[first_name last_name], result['allNames'].keys
649 | end
650 |
651 | test 'applying key_format! deeply can be disabled per scope' do
652 | names = { first_name: 'camel', last_name: 'case' }
653 | result = jbuild do |json|
654 | json.key_format! camelize: :lower
655 | json.deep_format_keys!
656 | json.set! :all_names, names
657 | json.scope do
658 | json.deep_format_keys! false
659 | json.set! :all_names, names
660 | end
661 | end
662 |
663 | assert_equal %w[firstName lastName], result['allNames'].keys
664 | assert_equal %i[first_name last_name], result['scope']['allNames'].keys
665 | end
666 |
667 | test 'applying key_format! deeply can be enabled globally' do
668 | names = { first_name: 'camel', last_name: 'case' }
669 |
670 | Jbuilder.deep_format_keys true
671 | result = jbuild do |json|
672 | json.key_format! camelize: :lower
673 | json.set! :all_names, names
674 | end
675 |
676 | assert_equal %w[firstName lastName], result['allNames'].keys
677 | Jbuilder.send(:class_variable_set, '@@deep_format_keys', false)
678 | end
679 |
680 | test 'deep key_format! with merge!' do
681 | hash = { camel_style: 'for JS' }
682 | result = jbuild do |json|
683 | json.key_format! camelize: :lower
684 | json.deep_format_keys!
685 | json.merge! hash
686 | end
687 |
688 | assert_equal ['camelStyle'], result.keys
689 | end
690 |
691 | test 'deep key_format! with merge! deep' do
692 | hash = { camel_style: { sub_attr: 'for JS' } }
693 | result = jbuild do |json|
694 | json.key_format! camelize: :lower
695 | json.deep_format_keys!
696 | json.merge! hash
697 | end
698 |
699 | assert_equal ['subAttr'], result['camelStyle'].keys
700 | end
701 |
702 | test 'deep key_format! with set! array of hashes' do
703 | names = [{ first_name: 'camel', last_name: 'case' }]
704 | result = jbuild do |json|
705 | json.key_format! camelize: :lower
706 | json.deep_format_keys!
707 | json.set! :names, names
708 | end
709 |
710 | assert_equal %w[firstName lastName], result['names'][0].keys
711 | end
712 |
713 | test 'deep key_format! with set! extracting hash from object' do
714 | comment = Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })
715 | result = jbuild do |json|
716 | json.key_format! camelize: :lower
717 | json.deep_format_keys!
718 | json.set! :comment, comment, :author
719 | end
720 |
721 | assert_equal %w[firstName lastName], result['comment']['author'].keys
722 | end
723 |
724 | test 'deep key_format! with array! of hashes' do
725 | names = [{ first_name: 'camel', last_name: 'case' }]
726 | result = jbuild do |json|
727 | json.key_format! camelize: :lower
728 | json.deep_format_keys!
729 | json.array! names
730 | end
731 |
732 | assert_equal %w[firstName lastName], result[0].keys
733 | end
734 |
735 | test 'deep key_format! with merge! array of hashes' do
736 | names = [{ first_name: 'camel', last_name: 'case' }]
737 | new_names = [{ first_name: 'snake', last_name: 'case' }]
738 | result = jbuild do |json|
739 | json.key_format! camelize: :lower
740 | json.deep_format_keys!
741 | json.array! names
742 | json.merge! new_names
743 | end
744 |
745 | assert_equal %w[firstName lastName], result[1].keys
746 | end
747 |
748 | test 'deep key_format! is applied to hash extracted from object' do
749 | comment = Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })
750 | result = jbuild do |json|
751 | json.key_format! camelize: :lower
752 | json.deep_format_keys!
753 | json.extract! comment, :author
754 | end
755 |
756 | assert_equal %w[firstName lastName], result['author'].keys
757 | end
758 |
759 | test 'deep key_format! is applied to hash extracted from hash' do
760 | comment = {author: { first_name: 'camel', last_name: 'case' }}
761 | result = jbuild do |json|
762 | json.key_format! camelize: :lower
763 | json.deep_format_keys!
764 | json.extract! comment, :author
765 | end
766 |
767 | assert_equal %w[firstName lastName], result['author'].keys
768 | end
769 |
770 | test 'deep key_format! is applied to hash extracted directly from array' do
771 | comments = [Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })]
772 | result = jbuild do |json|
773 | json.key_format! camelize: :lower
774 | json.deep_format_keys!
775 | json.array! comments, :author
776 | end
777 |
778 | assert_equal %w[firstName lastName], result[0]['author'].keys
779 | end
780 |
781 | test 'default key_format!' do
782 | Jbuilder.key_format camelize: :lower
783 | result = jbuild{ |json| json.camel_style 'for JS' }
784 | assert_equal ['camelStyle'], result.keys
785 | end
786 |
787 | test 'do not use default key formatter directly' do
788 | Jbuilder.key_format
789 | jbuild{ |json| json.key 'value' }
790 | formatter = Jbuilder.send(:class_variable_get, '@@key_formatter')
791 | cache = formatter.instance_variable_get('@cache')
792 | assert_empty cache
793 | end
794 |
795 | test 'ignore_nil! without a parameter' do
796 | result = jbuild do |json|
797 | json.ignore_nil!
798 | json.test nil
799 | end
800 |
801 | assert_empty result.keys
802 | end
803 |
804 | test 'ignore_nil! with parameter' do
805 | result = jbuild do |json|
806 | json.ignore_nil! true
807 | json.name 'Bob'
808 | json.dne nil
809 | end
810 |
811 | assert_equal ['name'], result.keys
812 |
813 | result = jbuild do |json|
814 | json.ignore_nil! false
815 | json.name 'Bob'
816 | json.dne nil
817 | end
818 |
819 | assert_equal ['name', 'dne'], result.keys
820 | end
821 |
822 | test 'default ignore_nil!' do
823 | Jbuilder.ignore_nil
824 |
825 | result = jbuild do |json|
826 | json.name 'Bob'
827 | json.dne nil
828 | end
829 |
830 | assert_equal ['name'], result.keys
831 | Jbuilder.send(:class_variable_set, '@@ignore_nil', false)
832 | end
833 |
834 | test 'nil!' do
835 | result = jbuild do |json|
836 | json.key 'value'
837 | json.nil!
838 | end
839 |
840 | assert_nil result
841 | end
842 |
843 | test 'null!' do
844 | result = jbuild do |json|
845 | json.key 'value'
846 | json.null!
847 | end
848 |
849 | assert_nil result
850 | end
851 |
852 | test 'null! in a block' do
853 | result = jbuild do |json|
854 | json.author do
855 | json.name 'David'
856 | end
857 |
858 | json.author do
859 | json.null!
860 | end
861 | end
862 |
863 | assert result.key?('author')
864 | assert_nil result['author']
865 | end
866 |
867 | test 'empty attributes respond to empty?' do
868 | attributes = Jbuilder.new.attributes!
869 | assert attributes.empty?
870 | assert attributes.blank?
871 | assert !attributes.present?
872 | end
873 |
874 | test 'throws ArrayError when trying to add a key to an array' do
875 | assert_raise Jbuilder::ArrayError do
876 | jbuild do |json|
877 | json.array! %w[foo bar]
878 | json.fizz "buzz"
879 | end
880 | end
881 | end
882 |
883 | test 'throws NullError when trying to add properties to null' do
884 | assert_raise Jbuilder::NullError do
885 | jbuild do |json|
886 | json.null!
887 | json.foo 'bar'
888 | end
889 | end
890 | end
891 |
892 | test 'throws NullError when trying to add properties to null using block syntax' do
893 | assert_raise Jbuilder::NullError do
894 | jbuild do |json|
895 | json.author do
896 | json.null!
897 | end
898 |
899 | json.author do
900 | json.name "Pavel"
901 | end
902 | end
903 | end
904 | end
905 |
906 | test "throws MergeError when trying to merge array with non-empty hash" do
907 | assert_raise Jbuilder::MergeError do
908 | jbuild do |json|
909 | json.name "Daniel"
910 | json.merge! []
911 | end
912 | end
913 | end
914 |
915 | test "throws MergeError when trying to merge hash with array" do
916 | assert_raise Jbuilder::MergeError do
917 | jbuild do |json|
918 | json.array!
919 | json.merge!({})
920 | end
921 | end
922 | end
923 |
924 | test "throws MergeError when trying to merge invalid objects" do
925 | assert_raise Jbuilder::MergeError do
926 | jbuild do |json|
927 | json.name "Daniel"
928 | json.merge! "Nope"
929 | end
930 | end
931 | end
932 |
933 | if RUBY_VERSION >= "2.2.10"
934 | test "respects JSON encoding customizations" do
935 | # Active Support overrides Time#as_json for custom formatting.
936 | # Ensure we call #to_json on the final attributes instead of JSON.dump.
937 | result = JSON.load(Jbuilder.encode { |json| json.time Time.parse("2018-05-13 11:51:00.485 -0400") })
938 | assert_equal "2018-05-13T11:51:00.485-04:00", result["time"]
939 | end
940 | end
941 | end
942 |
--------------------------------------------------------------------------------
/test/scaffold_api_controller_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'rails/generators/test_case'
3 | require 'generators/rails/scaffold_controller_generator'
4 |
5 | if Rails::VERSION::MAJOR > 4
6 |
7 | class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase
8 | tests Rails::Generators::ScaffoldControllerGenerator
9 | arguments %w(Post title body:text images:attachments --api)
10 | destination File.expand_path('../tmp', __FILE__)
11 | setup :prepare_destination
12 |
13 | test 'controller content' do
14 | run_generator
15 |
16 | assert_file 'app/controllers/posts_controller.rb' do |content|
17 | assert_instance_method :index, content do |m|
18 | assert_match %r{@posts = Post\.all}, m
19 | end
20 |
21 | assert_instance_method :show, content do |m|
22 | assert m.blank?
23 | end
24 |
25 | assert_instance_method :create, content do |m|
26 | assert_match %r{@post = Post\.new\(post_params\)}, m
27 | assert_match %r{@post\.save}, m
28 | assert_match %r{render :show, status: :created, location: @post}, m
29 | assert_match %r{render json: @post\.errors, status: :unprocessable_entity}, m
30 | end
31 |
32 | assert_instance_method :update, content do |m|
33 | assert_match %r{render :show, status: :ok, location: @post}, m
34 | assert_match %r{render json: @post.errors, status: :unprocessable_entity}, m
35 | end
36 |
37 | assert_instance_method :destroy, content do |m|
38 | assert_match %r{@post\.destroy}, m
39 | end
40 |
41 | assert_match %r{def set_post}, content
42 | if Rails::VERSION::MAJOR >= 8
43 | assert_match %r{params\.expect\(:id\)}, content
44 | else
45 | assert_match %r{params\[:id\]}, content
46 | end
47 |
48 | assert_match %r{def post_params}, content
49 | if Rails::VERSION::MAJOR >= 8
50 | assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content
51 | elsif Rails::VERSION::MAJOR >= 6
52 | assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content
53 | else
54 | assert_match %r{params\.require\(:post\)\.permit\(:title, :body, :images\)}, content
55 | end
56 | end
57 | end
58 |
59 | test "don't use require and permit if there are no attributes" do
60 | run_generator %w(Post --api)
61 |
62 | assert_file 'app/controllers/posts_controller.rb' do |content|
63 | assert_match %r{def post_params}, content
64 | assert_match %r{params\.fetch\(:post, \{\}\)}, content
65 | end
66 | end
67 |
68 |
69 | if Rails::VERSION::MAJOR >= 6
70 | test 'handles virtual attributes' do
71 | run_generator ["Message", "content:rich_text", "video:attachment", "photos:attachments"]
72 |
73 | assert_file 'app/controllers/messages_controller.rb' do |content|
74 | if Rails::VERSION::MAJOR >= 8
75 | assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content
76 | else
77 | assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content
78 | end
79 | end
80 | end
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/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 images:attachments)
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 %r{@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 %r{@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 %r{@post = Post\.new\(post_params\)}, m
33 | assert_match %r{@post\.save}, m
34 | assert_match %r{format\.html \{ redirect_to @post, notice: "Post was successfully created\." \}}, m
35 | assert_match %r{format\.json \{ render :show, status: :created, location: @post \}}, m
36 | assert_match %r{format\.html \{ render :new, status: :unprocessable_entity \}}, m
37 | assert_match %r{format\.json \{ render json: @post\.errors, status: :unprocessable_entity \}}, m
38 | end
39 |
40 | assert_instance_method :update, content do |m|
41 | assert_match %r{format\.html \{ redirect_to @post, notice: "Post was successfully updated\.", status: :see_other \}}, m
42 | assert_match %r{format\.json \{ render :show, status: :ok, location: @post \}}, m
43 | assert_match %r{format\.html \{ render :edit, status: :unprocessable_entity \}}, m
44 | assert_match %r{format\.json \{ render json: @post.errors, status: :unprocessable_entity \}}, m
45 | end
46 |
47 | assert_instance_method :destroy, content do |m|
48 | assert_match %r{@post\.destroy}, m
49 | assert_match %r{format\.html \{ redirect_to posts_path, notice: "Post was successfully destroyed\.", status: :see_other \}}, m
50 | assert_match %r{format\.json \{ head :no_content \}}, m
51 | end
52 |
53 | assert_match %r{def set_post}, content
54 | if Rails::VERSION::MAJOR >= 8
55 | assert_match %r{params\.expect\(:id\)}, content
56 | else
57 | assert_match %r{params\[:id\]}, content
58 | end
59 |
60 | assert_match %r{def post_params}, content
61 | if Rails::VERSION::MAJOR >= 8
62 | assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content
63 | elsif Rails::VERSION::MAJOR >= 6
64 | assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content
65 | else
66 | assert_match %r{params\.require\(:post\)\.permit\(:title, :body, :images\)}, content
67 | end
68 | end
69 | end
70 |
71 | if Rails::VERSION::MAJOR >= 6
72 | test 'controller with namespace' do
73 | run_generator %w(Admin::Post --model-name=Post)
74 | assert_file 'app/controllers/admin/posts_controller.rb' do |content|
75 | assert_instance_method :create, content do |m|
76 | assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully created\." \}}, m
77 | end
78 |
79 | assert_instance_method :update, content do |m|
80 | assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully updated\.", status: :see_other \}}, m
81 | end
82 |
83 | assert_instance_method :destroy, content do |m|
84 | assert_match %r{format\.html \{ redirect_to admin_posts_path, notice: "Post was successfully destroyed\.", status: :see_other \}}, m
85 | end
86 | end
87 | end
88 | end
89 |
90 | test "don't use require and permit if there are no attributes" do
91 | run_generator %w(Post)
92 |
93 | assert_file 'app/controllers/posts_controller.rb' do |content|
94 | assert_match %r{def post_params}, content
95 | assert_match %r{params\.fetch\(:post, \{\}\)}, content
96 | end
97 | end
98 |
99 | if Rails::VERSION::MAJOR >= 6
100 | test 'handles virtual attributes' do
101 | run_generator %w(Message content:rich_text video:attachment photos:attachments)
102 |
103 | assert_file 'app/controllers/messages_controller.rb' do |content|
104 | if Rails::VERSION::MAJOR >= 8
105 | assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content
106 | else
107 | assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content
108 | end
109 | end
110 | end
111 | end
112 | end
113 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 |
3 | require "rails"
4 |
5 | require "jbuilder"
6 |
7 | require "active_support/core_ext/array/access"
8 | require "active_support/cache/memory_store"
9 | require "active_support/json"
10 | require "active_model"
11 | require 'action_controller/railtie'
12 | require 'action_view/railtie'
13 |
14 | require "active_support/testing/autorun"
15 | require "mocha/minitest"
16 |
17 | ActiveSupport.test_order = :random
18 |
19 | ENV["RAILS_ENV"] ||= "test"
20 |
21 | class << Rails
22 | def cache
23 | @cache ||= ActiveSupport::Cache::MemoryStore.new
24 | end
25 | end
26 |
27 | Jbuilder::CollectionRenderer.collection_cache = Rails.cache
28 |
29 | class Post < Struct.new(:id, :body, :author_name)
30 | def cache_key
31 | "post-#{id}"
32 | end
33 | end
34 |
35 | class Racer < Struct.new(:id, :name)
36 | extend ActiveModel::Naming
37 | include ActiveModel::Conversion
38 | end
39 |
40 | # Instantiate an Application in order to trigger the initializers
41 | Class.new(Rails::Application) do
42 | config.secret_key_base = 'secret'
43 | config.eager_load = false
44 | end.initialize!
45 |
46 | # Touch AV::Base in order to trigger :action_view on_load hook before running the tests
47 | ActionView::Base.inspect
48 |
--------------------------------------------------------------------------------