diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..96f29c19 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ruby +{ + "name": "jbuilder", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "ghcr.io/rails/devcontainer/images/ruby:3.4.5", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "ruby --version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 0d5374dd..3bc499ad 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -24,14 +24,19 @@ jobs: gemfile: - "rails_7_0" - "rails_7_1" + - "rails_7_2" - "rails_8_0" - "rails_head" exclude: + - ruby: '3.0' + gemfile: rails_7_2 - ruby: '3.0' gemfile: rails_8_0 - ruby: '3.0' gemfile: rails_head + - ruby: '3.1' + gemfile: rails_7_2 - ruby: '3.1' gemfile: rails_8_0 - ruby: '3.1' diff --git a/Appraisals b/Appraisals index 516847c7..3caeb7ef 100644 --- a/Appraisals +++ b/Appraisals @@ -1,3 +1,5 @@ +# frozen_string_literal: true + appraise "rails-7-0" do gem "rails", "~> 7.0.0" gem "concurrent-ruby", "< 1.3.5" # to avoid problem described in https://github.com/rails/rails/pull/54264 @@ -7,6 +9,10 @@ appraise "rails-7-1" do gem "rails", "~> 7.1.0" end +appraise "rails-7-2" do + gem "rails", "~> 7.2.0" +end + appraise "rails-8-0" do gem "rails", "~> 8.0.0" end diff --git a/Gemfile b/Gemfile index 77021588..02c3b379 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" gemspec diff --git a/Rakefile b/Rakefile index 3a28e6fa..4b82612e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/setup" require "bundler/gem_tasks" require "rake/testtask" diff --git a/bin/test b/bin/test index 45d4fe95..1bc5120f 100755 --- a/bin/test +++ b/bin/test @@ -1,4 +1,4 @@ -#!/bin/env bash +#!/usr/bin/env bash set -e bundle install diff --git a/gemfiles/rails_7_2.gemfile b/gemfiles/rails_7_2.gemfile new file mode 100644 index 00000000..8f5a412b --- /dev/null +++ b/gemfiles/rails_7_2.gemfile @@ -0,0 +1,10 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rake" +gem "mocha", require: false +gem "appraisal" +gem "rails", "~> 7.2.0" + +gemspec path: "../" diff --git a/jbuilder.gemspec b/jbuilder.gemspec index 0b519531..90e1176f 100644 --- a/jbuilder.gemspec +++ b/jbuilder.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "lib/jbuilder/version" Gem::Specification.new do |s| @@ -9,10 +11,10 @@ Gem::Specification.new do |s| s.homepage = 'https://github.com/rails/jbuilder' s.license = 'MIT' - s.required_ruby_version = '>= 2.2.2' + s.required_ruby_version = '>= 3.0.0' - s.add_dependency 'activesupport', '>= 5.0.0' - s.add_dependency 'actionview', '>= 5.0.0' + s.add_dependency 'activesupport', '>= 7.0.0' + s.add_dependency 'actionview', '>= 7.0.0' if RUBY_ENGINE == 'rbx' s.add_development_dependency('racc') diff --git a/lib/generators/rails/jbuilder_generator.rb b/lib/generators/rails/jbuilder_generator.rb index 79f742e8..81eb54e7 100644 --- a/lib/generators/rails/jbuilder_generator.rb +++ b/lib/generators/rails/jbuilder_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails/generators/named_base' require 'rails/generators/resource_helpers' diff --git a/lib/generators/rails/scaffold_controller_generator.rb b/lib/generators/rails/scaffold_controller_generator.rb index bf48f0cb..42407c09 100644 --- a/lib/generators/rails/scaffold_controller_generator.rb +++ b/lib/generators/rails/scaffold_controller_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails/generators' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index b12624be..1290839f 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + require 'active_support' require 'jbuilder/jbuilder' require 'jbuilder/blank' require 'jbuilder/key_formatter' require 'jbuilder/errors' -require 'jbuilder/version' require 'json' require 'active_support/core_ext/hash/deep_merge' @@ -12,19 +13,23 @@ class Jbuilder @@ignore_nil = false @@deep_format_keys = false - def initialize(options = {}) + def initialize( + key_formatter: @@key_formatter, + ignore_nil: @@ignore_nil, + deep_format_keys: @@deep_format_keys, + &block + ) @attributes = {} + @key_formatter = key_formatter + @ignore_nil = ignore_nil + @deep_format_keys = deep_format_keys - @key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil} - @ignore_nil = options.fetch(:ignore_nil, @@ignore_nil) - @deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys) - - yield self if ::Kernel.block_given? + yield self if block end # Yields a builder and automatically turns the result into a JSON string - def self.encode(*args, &block) - new(*args, &block).target! + def self.encode(...) + new(...).target! end BLANK = Blank.new @@ -58,20 +63,12 @@ def set!(key, value = BLANK, *args, &block) else # json.author @post.creator, :name, :email_address # { "author": { "name": "David", "email_address": "david@loudthinking.com" } } - _merge_block(key){ extract! value, *args } + _merge_block(key){ _extract value, args } end _set_value key, result end - def method_missing(*args, &block) - if ::Kernel.block_given? - set!(*args, &block) - else - set!(*args) - end - end - # Specifies formatting to be applied to the key. Passing in a name of a function # will cause that function to be called on the key. So :upcase will upper case # the key. You can also pass in lambdas for more complex transformations. @@ -100,13 +97,13 @@ def method_missing(*args, &block) # # { "_first_name": "David" } # - def key_format!(*args) - @key_formatter = KeyFormatter.new(*args) + def key_format!(...) + @key_formatter = KeyFormatter.new(...) end # Same as the instance method key_format! except sets the default. - def self.key_format(*args) - @@key_formatter = KeyFormatter.new(*args) + def self.key_format(...) + @@key_formatter = KeyFormatter.new(...) end # If you want to skip adding nil values to your JSON hash. This is useful @@ -215,7 +212,7 @@ def array!(collection = [], *attributes, &block) elsif ::Kernel.block_given? _map_collection(collection, &block) elsif attributes.any? - _map_collection(collection) { |element| extract! element, *attributes } + _map_collection(collection) { |element| _extract element, attributes } else _format_keys(collection.to_a) end @@ -241,18 +238,14 @@ def array!(collection = [], *attributes, &block) # # json.(@person, :name, :age) def extract!(object, *attributes) - if ::Hash === object - _extract_hash_values(object, attributes) - else - _extract_method_values(object, attributes) - end + _extract object, attributes end def call(object, *attributes, &block) if ::Kernel.block_given? array! object, &block else - extract! object, *attributes + _extract object, attributes end end @@ -281,6 +274,16 @@ def target! private + alias_method :method_missing, :set! + + def _extract(object, attributes) + if ::Hash === object + _extract_hash_values(object, attributes) + else + _extract_method_values(object, attributes) + end + end + def _extract_hash_values(object, attributes) attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) } end @@ -311,7 +314,13 @@ def _merge_values(current_value, updates) end def _key(key) - @key_formatter ? @key_formatter.format(key) : key.to_s + if @key_formatter + @key_formatter.format(key) + elsif key.is_a?(::Symbol) + key.name + else + key.to_s + end end def _format_keys(hash_or_array) @@ -350,16 +359,12 @@ def _scope end def _is_collection?(object) - _object_respond_to?(object, :map, :count) && !(::Struct === object) + object.respond_to?(:map) && object.respond_to?(:count) && !(::Struct === object) end def _blank?(value=@attributes) BLANK == value end - - def _object_respond_to?(object, *methods) - methods.all?{ |m| object.respond_to?(m) } - end end require 'jbuilder/railtie' if defined?(Rails) diff --git a/lib/jbuilder/blank.rb b/lib/jbuilder/blank.rb index 52af2c6f..41169135 100644 --- a/lib/jbuilder/blank.rb +++ b/lib/jbuilder/blank.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Jbuilder class Blank def ==(other) diff --git a/lib/jbuilder/collection_renderer.rb b/lib/jbuilder/collection_renderer.rb index 48707bfe..85844eec 100644 --- a/lib/jbuilder/collection_renderer.rb +++ b/lib/jbuilder/collection_renderer.rb @@ -1,37 +1,11 @@ +# frozen_string_literal: true + require 'delegate' -require 'active_support/concern' require 'action_view' - -begin - require 'action_view/renderer/collection_renderer' -rescue LoadError - require 'action_view/renderer/partial_renderer' -end +require 'action_view/renderer/collection_renderer' class Jbuilder - module CollectionRenderable # :nodoc: - extend ActiveSupport::Concern - - class_methods do - def supported? - superclass.private_method_defined?(:build_rendered_template) && self.superclass.private_method_defined?(:build_rendered_collection) - end - end - - private - - def build_rendered_template(content, template, layout = nil) - super(content || json.attributes!, template) - end - - def build_rendered_collection(templates, _spacer) - json.merge!(templates.map(&:body)) - end - - def json - @options[:locals].fetch(:json) - end - + class CollectionRenderer < ::ActionView::CollectionRenderer # :nodoc: class ScopedIterator < ::SimpleDelegator # :nodoc: include Enumerable @@ -40,16 +14,6 @@ def initialize(obj, scope) @scope = scope end - # Rails 6.0 support: - def each - return enum_for(:each) unless block_given? - - __getobj__.each do |object| - @scope.call { yield(object) } - end - end - - # Rails 6.1 support: def each_with_info return enum_for(:each_with_info) unless block_given? @@ -60,51 +24,29 @@ def each_with_info end private_constant :ScopedIterator - end - - if defined?(::ActionView::CollectionRenderer) - # Rails 6.1 support: - class CollectionRenderer < ::ActionView::CollectionRenderer # :nodoc: - include CollectionRenderable - def initialize(lookup_context, options, &scope) - super(lookup_context, options) - @scope = scope - end - - private - def collection_with_template(view, template, layout, collection) - super(view, template, layout, ScopedIterator.new(collection, @scope)) - end + def initialize(lookup_context, options, &scope) + super(lookup_context, options) + @scope = scope end - else - # Rails 6.0 support: - class CollectionRenderer < ::ActionView::PartialRenderer # :nodoc: - include CollectionRenderable - def initialize(lookup_context, options, &scope) - super(lookup_context) - @options = options - @scope = scope - end + private - def render_collection_with_partial(collection, partial, context, block) - render(context, @options.merge(collection: collection, partial: partial), block) + def build_rendered_template(content, template, layout = nil) + super(content || json.attributes!, template) end - private - def collection_without_template(view) - @collection = ScopedIterator.new(@collection, @scope) - - super(view) - end + def build_rendered_collection(templates, _spacer) + json.merge!(templates.map(&:body)) + end - def collection_with_template(view, template) - @collection = ScopedIterator.new(@collection, @scope) + def json + @options[:locals].fetch(:json) + end - super(view, template) - end - end + def collection_with_template(view, template, layout, collection) + super(view, template, layout, ScopedIterator.new(collection, @scope)) + end end class EnumerableCompat < ::SimpleDelegator diff --git a/lib/jbuilder/errors.rb b/lib/jbuilder/errors.rb index 386e6da8..aba1f89b 100644 --- a/lib/jbuilder/errors.rb +++ b/lib/jbuilder/errors.rb @@ -1,4 +1,6 @@ -require 'jbuilder/jbuilder' +# frozen_string_literal: true + +require 'jbuilder/version' class Jbuilder class NullError < ::NoMethodError diff --git a/lib/jbuilder/jbuilder.rb b/lib/jbuilder/jbuilder.rb index 22b0ac4e..ddb52733 100644 --- a/lib/jbuilder/jbuilder.rb +++ b/lib/jbuilder/jbuilder.rb @@ -1 +1,3 @@ -Jbuilder = Class.new(BasicObject) +# frozen_string_literal: true + +require 'jbuilder/version' diff --git a/lib/jbuilder/jbuilder_dependency_tracker.rb b/lib/jbuilder/jbuilder_dependency_tracker.rb index 62b6dbf2..b37fade0 100644 --- a/lib/jbuilder/jbuilder_dependency_tracker.rb +++ b/lib/jbuilder/jbuilder_dependency_tracker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Jbuilder::DependencyTracker EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/ diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 55f2d5ff..855dd678 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'jbuilder/jbuilder' require 'jbuilder/collection_renderer' require 'action_dispatch/http/mime_type' @@ -10,10 +12,11 @@ class << self self.template_lookup_options = { handlers: [:jbuilder] } - def initialize(context, *args) + def initialize(context, options = nil) @context = context @cached_root = nil - super(*args) + + options.nil? ? super() : super(**options) end # Generates JSON using the template specified with the `:partial` option. For example, the code below will render @@ -52,7 +55,9 @@ def partial!(*args) if args.one? && _is_active_model?(args.first) _render_active_model_partial args.first else - _render_explicit_partial(*args) + options = args.extract_options!.dup + options[:partial] = args.first if args.present? + _render_partial_with_options options end end @@ -118,7 +123,9 @@ def array!(collection = [], *args) options = args.first if args.one? && _partial_options?(options) - partial! options.merge(collection: collection) + options = options.dup + options[:collection] = collection + _render_partial_with_options options else super end @@ -128,7 +135,7 @@ def set!(name, object = BLANK, *args) options = args.first if args.one? && _partial_options?(options) - _set_inline_partial name, object, options + _set_inline_partial name, object, options.dup else super end @@ -136,15 +143,17 @@ def set!(name, object = BLANK, *args) private + alias_method :method_missing, :set! + def _render_partial_with_options(options) - options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached) - options.reverse_merge! ::JbuilderTemplate.template_lookup_options + options[:locals] ||= options.except(:partial, :as, :collection, :cached) + options[:handlers] ||= ::JbuilderTemplate.template_lookup_options[:handlers] as = options[:as] - if as && options.key?(:collection) && CollectionRenderer.supported? + if as && options.key?(:collection) collection = options.delete(:collection) || [] partial = options.delete(:partial) - options[:locals].merge!(json: self) + options[:locals][:json] = self collection = EnumerableCompat.new(collection) if collection.respond_to?(:count) && !collection.respond_to?(:size) if options.has_key?(:layout) @@ -164,29 +173,13 @@ def _render_partial_with_options(options) else array! end - elsif as && options.key?(:collection) && !CollectionRenderer.supported? - # For Rails <= 5.2: - as = as.to_sym - collection = options.delete(:collection) - - if collection.present? - locals = options.delete(:locals) - array! collection do |member| - member_locals = locals.clone - member_locals.merge! collection: collection - member_locals.merge! as => member - _render_partial options.merge(locals: member_locals) - end - else - array! - end else _render_partial options end end def _render_partial(options) - options[:locals].merge! json: self + options[:locals][:json] = self @context.render options end @@ -242,34 +235,18 @@ def _set_inline_partial(name, object, options) value = if object.nil? [] elsif _is_collection?(object) - _scope{ _render_partial_with_options options.merge(collection: object) } - else - locals = ::Hash[options[:as], object] - _scope{ _render_partial_with_options options.merge(locals: locals) } - end - - set! name, value - end - - def _render_explicit_partial(name_or_options, locals = {}) - case name_or_options - when ::Hash - # partial! partial: 'name', foo: 'bar' - options = name_or_options + _scope do + options[:collection] = object + _render_partial_with_options options + end else - # partial! 'name', locals: {foo: 'bar'} - if locals.one? && (locals.keys.first == :locals) - options = locals.merge(partial: name_or_options) - else - options = { partial: name_or_options, locals: locals } + _scope do + options[:locals] = { options[:as] => object } + _render_partial_with_options options end - # partial! 'name', foo: 'bar' - as = locals.delete(:as) - options[:as] = as if as.present? - options[:collection] = locals[:collection] if locals.key?(:collection) end - _render_partial_with_options options + _set_value name, value end def _render_active_model_partial(object) diff --git a/lib/jbuilder/key_formatter.rb b/lib/jbuilder/key_formatter.rb index c9e7eca3..e73916bf 100644 --- a/lib/jbuilder/key_formatter.rb +++ b/lib/jbuilder/key_formatter.rb @@ -1,32 +1,30 @@ +# frozen_string_literal: true + require 'jbuilder/jbuilder' -require 'active_support/core_ext/array' class Jbuilder class KeyFormatter - def initialize(*args) - @format = {} - @cache = {} - - options = args.extract_options! - args.each do |name| - @format[name] = [] - end - options.each do |name, parameters| - @format[name] = parameters - end - end - - def initialize_copy(original) + def initialize(*formats, **formats_with_options) + @mutex = Mutex.new + @formats = formats + @formats_with_options = formats_with_options @cache = {} end def format(key) - @cache[key] ||= @format.inject(key.to_s) do |result, args| - func, args = args - if ::Proc === func - func.call result, *args - else - result.send func, *args + @mutex.synchronize do + @cache[key] ||= begin + value = key.is_a?(Symbol) ? key.name : key.to_s + + @formats.each do |func| + value = func.is_a?(Proc) ? func.call(value) : value.send(func) + end + + @formats_with_options.each do |func, params| + value = func.is_a?(Proc) ? func.call(value, *params) : value.send(func, *params) + end + + value end end end diff --git a/lib/jbuilder/railtie.rb b/lib/jbuilder/railtie.rb index 2aeefbb6..740462af 100644 --- a/lib/jbuilder/railtie.rb +++ b/lib/jbuilder/railtie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails' require 'jbuilder/jbuilder_template' @@ -9,28 +11,24 @@ class Railtie < ::Rails::Railtie require 'jbuilder/jbuilder_dependency_tracker' end - if Rails::VERSION::MAJOR >= 5 - module ::ActionController - module ApiRendering - include ActionView::Rendering - end + module ::ActionController + module ApiRendering + include ActionView::Rendering end + end - ActiveSupport.on_load :action_controller do - if name == 'ActionController::API' - include ActionController::Helpers - include ActionController::ImplicitRender - end - end + ActiveSupport.on_load :action_controller_api do + include ActionController::Helpers + include ActionController::ImplicitRender + helper_method :combined_fragment_cache_key + helper_method :view_cache_dependencies end end - if Rails::VERSION::MAJOR >= 4 - generators do |app| - Rails::Generators.configure! app.config.generators - Rails::Generators.hidden_namespaces.uniq! - require 'generators/rails/scaffold_controller_generator' - end + generators do |app| + Rails::Generators.configure! app.config.generators + Rails::Generators.hidden_namespaces.uniq! + require 'generators/rails/scaffold_controller_generator' end end end diff --git a/lib/jbuilder/version.rb b/lib/jbuilder/version.rb index 2a62782f..78151fd0 100644 --- a/lib/jbuilder/version.rb +++ b/lib/jbuilder/version.rb @@ -1,3 +1,5 @@ -class Jbuilder - VERSION = "2.13.0" +# frozen_string_literal: true + +class Jbuilder < BasicObject + VERSION = "2.14.1" end diff --git a/test/jbuilder_generator_test.rb b/test/jbuilder_generator_test.rb index e4a2f165..8b1ab9a8 100644 --- a/test/jbuilder_generator_test.rb +++ b/test/jbuilder_generator_test.rb @@ -56,15 +56,13 @@ class JbuilderGeneratorTest < Rails::Generators::TestCase end end - if Rails::VERSION::MAJOR >= 6 - test 'handles virtual attributes' do - run_generator %w(Message content:rich_text video:attachment photos:attachments) + test 'handles virtual attributes' do + run_generator %w(Message content:rich_text video:attachment photos:attachments) - assert_file 'app/views/messages/_message.json.jbuilder' do |content| - assert_match %r{json\.content message\.content\.to_s}, content - assert_match %r{json\.video url_for\(message\.video\)}, content - 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 - end + assert_file 'app/views/messages/_message.json.jbuilder' do |content| + assert_match %r{json\.content message\.content\.to_s}, content + assert_match %r{json\.video url_for\(message\.video\)}, content + 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 end end end diff --git a/test/jbuilder_template_test.rb b/test/jbuilder_template_test.rb index 54addb23..76368e12 100644 --- a/test/jbuilder_template_test.rb +++ b/test/jbuilder_template_test.rb @@ -317,99 +317,97 @@ class JbuilderTemplateTest < ActiveSupport::TestCase assert_equal "David", result["firstName"] end - if JbuilderTemplate::CollectionRenderer.supported? - test "returns an empty array for an empty collection" do - Jbuilder::CollectionRenderer.expects(:new).never - result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: []) + test "returns an empty array for an empty collection" do + Jbuilder::CollectionRenderer.expects(:new).never + result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: []) - # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. - assert_equal [], result - end + # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. + assert_equal [], result + end - test "works with an enumerable object" do - enumerable_class = Class.new do - include Enumerable + test "works with an enumerable object" do + enumerable_class = Class.new do + include Enumerable - def each(&block) - [].each(&block) - end + def each(&block) + [].each(&block) end + end - result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: enumerable_class.new) + result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: enumerable_class.new) - # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. - assert_equal [], result - end + # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. + assert_equal [], result + end + + test "supports the cached: true option" do + result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS) - test "supports the cached: true option" do - result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS) - - assert_equal 10, result.count - assert_equal "Post #5", result[4]["body"] - assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] - assert_equal "Pavel", result[5]["author"]["first_name"] - - expected = { - "id" => 1, - "body" => "Post #1", - "author" => { - "first_name" => "David", - "last_name" => "Heinemeier Hansson" - } + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] + + expected = { + "id" => 1, + "body" => "Post #1", + "author" => { + "first_name" => "David", + "last_name" => "Heinemeier Hansson" } + } - assert_equal expected, Rails.cache.read("post-1") + assert_equal expected, Rails.cache.read("post-1") - result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS) + result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS) - assert_equal 10, result.count - assert_equal "Post #5", result[4]["body"] - assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] - assert_equal "Pavel", result[5]["author"]["first_name"] - end + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] + end - test "supports the cached: ->() {} option" do - result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS) - - assert_equal 10, result.count - assert_equal "Post #5", result[4]["body"] - assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] - assert_equal "Pavel", result[5]["author"]["first_name"] - - expected = { - "id" => 1, - "body" => "Post #1", - "author" => { - "first_name" => "David", - "last_name" => "Heinemeier Hansson" - } - } + test "supports the cached: ->() {} option" do + result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS) - assert_equal expected, Rails.cache.read("post-1/foo") + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] - result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS) + expected = { + "id" => 1, + "body" => "Post #1", + "author" => { + "first_name" => "David", + "last_name" => "Heinemeier Hansson" + } + } - assert_equal 10, result.count - assert_equal "Post #5", result[4]["body"] - assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] - assert_equal "Pavel", result[5]["author"]["first_name"] - end + assert_equal expected, Rails.cache.read("post-1/foo") - test "raises an error on a render call with the :layout option" do - error = assert_raises NotImplementedError do - render('json.array! @posts, partial: "post", as: :post, layout: "layout"', posts: POSTS) - end + result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS) + + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] + end - assert_equal "The `:layout' option is not supported in collection rendering.", error.message + test "raises an error on a render call with the :layout option" do + error = assert_raises NotImplementedError do + render('json.array! @posts, partial: "post", as: :post, layout: "layout"', posts: POSTS) end - test "raises an error on a render call with the :spacer_template option" do - error = assert_raises NotImplementedError do - render('json.array! @posts, partial: "post", as: :post, spacer_template: "template"', posts: POSTS) - end + assert_equal "The `:layout' option is not supported in collection rendering.", error.message + end - assert_equal "The `:spacer_template' option is not supported in collection rendering.", error.message + test "raises an error on a render call with the :spacer_template option" do + error = assert_raises NotImplementedError do + render('json.array! @posts, partial: "post", as: :post, spacer_template: "template"', posts: POSTS) end + + assert_equal "The `:spacer_template' option is not supported in collection rendering.", error.message end private @@ -427,12 +425,7 @@ def build_view(options = {}) lookup_context = ActionView::LookupContext.new([ resolver ], {}, [""]) controller = ActionView::TestCase::TestController.new - # TODO: Use with_empty_template_cache unconditionally after dropping support for Rails <6.0. - view = if ActionView::Base.respond_to?(:with_empty_template_cache) - ActionView::Base.with_empty_template_cache.new(lookup_context, options.fetch(:assigns, {}), controller) - else - ActionView::Base.new(lookup_context, options.fetch(:assigns, {}), controller) - end + view = ActionView::Base.with_empty_template_cache.new(lookup_context, options.fetch(:assigns, {}), controller) def view.view_cache_dependencies; []; end def view.combined_fragment_cache_key(key) [ key ] end diff --git a/test/jbuilder_test.rb b/test/jbuilder_test.rb index 76569bb6..c01fb1e8 100644 --- a/test/jbuilder_test.rb +++ b/test/jbuilder_test.rb @@ -784,12 +784,12 @@ class JbuilderTest < ActiveSupport::TestCase assert_equal ['camelStyle'], result.keys end - test 'do not use default key formatter directly' do + test 'use default key formatter when configured' do Jbuilder.key_format jbuild{ |json| json.key 'value' } formatter = Jbuilder.send(:class_variable_get, '@@key_formatter') cache = formatter.instance_variable_get('@cache') - assert_empty cache + assert_includes cache, :key end test 'ignore_nil! without a parameter' do @@ -930,12 +930,17 @@ class JbuilderTest < ActiveSupport::TestCase end end - if RUBY_VERSION >= "2.2.10" - test "respects JSON encoding customizations" do - # Active Support overrides Time#as_json for custom formatting. - # Ensure we call #to_json on the final attributes instead of JSON.dump. - result = JSON.load(Jbuilder.encode { |json| json.time Time.parse("2018-05-13 11:51:00.485 -0400") }) - assert_equal "2018-05-13T11:51:00.485-04:00", result["time"] + test "respects JSON encoding customizations" do + # Active Support overrides Time#as_json for custom formatting. + # Ensure we call #to_json on the final attributes instead of JSON.dump. + result = JSON.load(Jbuilder.encode { |json| json.time Time.parse("2018-05-13 11:51:00.485 -0400") }) + assert_equal "2018-05-13T11:51:00.485-04:00", result["time"] + end + + test "encode forwards options to new" do + Jbuilder.encode(key_formatter: 1, ignore_nil: 2) do |json| + assert_equal 1, json.instance_eval{ @key_formatter } + assert_equal 2, json.instance_eval{ @ignore_nil } end end end diff --git a/test/scaffold_api_controller_generator_test.rb b/test/scaffold_api_controller_generator_test.rb index e7e2b355..546749b1 100644 --- a/test/scaffold_api_controller_generator_test.rb +++ b/test/scaffold_api_controller_generator_test.rb @@ -2,81 +2,73 @@ require 'rails/generators/test_case' require 'generators/rails/scaffold_controller_generator' -if Rails::VERSION::MAJOR > 4 +class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase + tests Rails::Generators::ScaffoldControllerGenerator + arguments %w(Post title body:text images:attachments --api --skip-routes) + destination File.expand_path('../tmp', __FILE__) + setup :prepare_destination - class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase - tests Rails::Generators::ScaffoldControllerGenerator - arguments %w(Post title body:text images:attachments --api) - destination File.expand_path('../tmp', __FILE__) - setup :prepare_destination + test 'controller content' do + run_generator - test 'controller content' do - run_generator - - assert_file 'app/controllers/posts_controller.rb' do |content| - assert_instance_method :index, content do |m| - assert_match %r{@posts = Post\.all}, m - end + assert_file 'app/controllers/posts_controller.rb' do |content| + assert_instance_method :index, content do |m| + assert_match %r{@posts = Post\.all}, m + end - assert_instance_method :show, content do |m| - assert m.blank? - end + assert_instance_method :show, content do |m| + assert m.blank? + end - assert_instance_method :create, content do |m| - assert_match %r{@post = Post\.new\(post_params\)}, m - assert_match %r{@post\.save}, m - assert_match %r{render :show, status: :created, location: @post}, m - assert_match %r{render json: @post\.errors, status: :unprocessable_entity}, m - end + assert_instance_method :create, content do |m| + assert_match %r{@post = Post\.new\(post_params\)}, m + assert_match %r{@post\.save}, m + assert_match %r{render :show, status: :created, location: @post}, m + assert_match %r{render json: @post\.errors, status: :unprocessable_entity}, m + end - assert_instance_method :update, content do |m| - assert_match %r{render :show, status: :ok, location: @post}, m - assert_match %r{render json: @post.errors, status: :unprocessable_entity}, m - end + assert_instance_method :update, content do |m| + assert_match %r{render :show, status: :ok, location: @post}, m + assert_match %r{render json: @post.errors, status: :unprocessable_entity}, m + end - assert_instance_method :destroy, content do |m| - assert_match %r{@post\.destroy}, m - end + assert_instance_method :destroy, content do |m| + assert_match %r{@post\.destroy}, m + end - assert_match %r{def set_post}, content - if Rails::VERSION::MAJOR >= 8 - assert_match %r{params\.expect\(:id\)}, content - else - assert_match %r{params\[:id\]}, content - end + assert_match %r{def set_post}, content + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(:id\)}, content + else + assert_match %r{params\[:id\]}, content + end - assert_match %r{def post_params}, content - if Rails::VERSION::MAJOR >= 8 - assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content - elsif Rails::VERSION::MAJOR >= 6 - assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content - else - assert_match %r{params\.require\(:post\)\.permit\(:title, :body, :images\)}, content - end + assert_match %r{def post_params}, content + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content + else + assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content end end + end - test "don't use require and permit if there are no attributes" do - run_generator %w(Post --api) + test "don't use require and permit if there are no attributes" do + run_generator %w(Post --api --skip-routes) - assert_file 'app/controllers/posts_controller.rb' do |content| - assert_match %r{def post_params}, content - assert_match %r{params\.fetch\(:post, \{\}\)}, content - end + assert_file 'app/controllers/posts_controller.rb' do |content| + assert_match %r{def post_params}, content + assert_match %r{params\.fetch\(:post, \{\}\)}, content end + end + test 'handles virtual attributes' do + run_generator %w(Message content:rich_text video:attachment photos:attachments --skip-routes) - if Rails::VERSION::MAJOR >= 6 - test 'handles virtual attributes' do - run_generator ["Message", "content:rich_text", "video:attachment", "photos:attachments"] - - assert_file 'app/controllers/messages_controller.rb' do |content| - if Rails::VERSION::MAJOR >= 8 - assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content - else - assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content - end - end + assert_file 'app/controllers/messages_controller.rb' do |content| + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content + else + assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content end end end diff --git a/test/scaffold_controller_generator_test.rb b/test/scaffold_controller_generator_test.rb index a642eef9..048df2a3 100644 --- a/test/scaffold_controller_generator_test.rb +++ b/test/scaffold_controller_generator_test.rb @@ -4,7 +4,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase tests Rails::Generators::ScaffoldControllerGenerator - arguments %w(Post title body:text images:attachments) + arguments %w(Post title body:text images:attachments --skip-routes) destination File.expand_path('../tmp', __FILE__) setup :prepare_destination @@ -60,35 +60,31 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_match %r{def post_params}, content if Rails::VERSION::MAJOR >= 8 assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content - elsif Rails::VERSION::MAJOR >= 6 - assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content else - assert_match %r{params\.require\(:post\)\.permit\(:title, :body, :images\)}, content + assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content end end end - if Rails::VERSION::MAJOR >= 6 - test 'controller with namespace' do - run_generator %w(Admin::Post --model-name=Post) - assert_file 'app/controllers/admin/posts_controller.rb' do |content| - assert_instance_method :create, content do |m| - assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully created\." \}}, m - end - - assert_instance_method :update, content do |m| - assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully updated\.", status: :see_other \}}, m - end - - assert_instance_method :destroy, content do |m| - assert_match %r{format\.html \{ redirect_to admin_posts_path, notice: "Post was successfully destroyed\.", status: :see_other \}}, m - end + test 'controller with namespace' do + run_generator %w(Admin::Post --model-name=Post --skip-routes) + assert_file 'app/controllers/admin/posts_controller.rb' do |content| + assert_instance_method :create, content do |m| + assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully created\." \}}, m + end + + assert_instance_method :update, content do |m| + assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully updated\.", status: :see_other \}}, m + end + + assert_instance_method :destroy, content do |m| + assert_match %r{format\.html \{ redirect_to admin_posts_path, notice: "Post was successfully destroyed\.", status: :see_other \}}, m end end end test "don't use require and permit if there are no attributes" do - run_generator %w(Post) + run_generator %w(Post --skip-routes) assert_file 'app/controllers/posts_controller.rb' do |content| assert_match %r{def post_params}, content @@ -96,16 +92,14 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end end - if Rails::VERSION::MAJOR >= 6 - test 'handles virtual attributes' do - run_generator %w(Message content:rich_text video:attachment photos:attachments) + test 'handles virtual attributes' do + run_generator %w(Message content:rich_text video:attachment photos:attachments --skip-routes) - assert_file 'app/controllers/messages_controller.rb' do |content| - if Rails::VERSION::MAJOR >= 8 - assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content - else - assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content - end + assert_file 'app/controllers/messages_controller.rb' do |content| + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content + else + assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index d985da9c..0b4dfa64 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,7 +19,7 @@ ENV["RAILS_ENV"] ||= "test" class << Rails - def cache + redefine_method :cache do @cache ||= ActiveSupport::Cache::MemoryStore.new end end