jbuilder 2.0.6 → 2.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +108 -0
  3. data/.gitignore +4 -1
  4. data/Appraisals +25 -0
  5. data/CONTRIBUTING.md +106 -0
  6. data/Gemfile +4 -12
  7. data/MIT-LICENSE +1 -1
  8. data/README.md +171 -45
  9. data/Rakefile +15 -10
  10. data/gemfiles/rails_5_0.gemfile +10 -0
  11. data/gemfiles/rails_5_1.gemfile +10 -0
  12. data/gemfiles/rails_5_2.gemfile +10 -0
  13. data/gemfiles/rails_6_0.gemfile +10 -0
  14. data/gemfiles/rails_6_1.gemfile +10 -0
  15. data/gemfiles/rails_head.gemfile +10 -0
  16. data/jbuilder.gemspec +20 -6
  17. data/lib/generators/rails/jbuilder_generator.rb +13 -2
  18. data/lib/generators/rails/scaffold_controller_generator.rb +9 -3
  19. data/lib/generators/rails/templates/api_controller.rb +63 -0
  20. data/lib/generators/rails/templates/controller.rb +16 -20
  21. data/lib/generators/rails/templates/index.json.jbuilder +1 -4
  22. data/lib/generators/rails/templates/partial.json.jbuilder +16 -0
  23. data/lib/generators/rails/templates/show.json.jbuilder +1 -1
  24. data/lib/jbuilder/blank.rb +11 -0
  25. data/lib/jbuilder/collection_renderer.rb +109 -0
  26. data/lib/jbuilder/dependency_tracker.rb +1 -1
  27. data/lib/jbuilder/errors.rb +24 -0
  28. data/lib/jbuilder/jbuilder.rb +7 -0
  29. data/lib/jbuilder/jbuilder_template.rb +213 -65
  30. data/lib/jbuilder/key_formatter.rb +34 -0
  31. data/lib/jbuilder/railtie.rb +31 -6
  32. data/lib/jbuilder.rb +148 -114
  33. data/test/jbuilder_dependency_tracker_test.rb +3 -4
  34. data/test/jbuilder_generator_test.rb +31 -4
  35. data/test/jbuilder_template_test.rb +313 -195
  36. data/test/jbuilder_test.rb +615 -219
  37. data/test/scaffold_api_controller_generator_test.rb +70 -0
  38. data/test/scaffold_controller_generator_test.rb +62 -19
  39. data/test/test_helper.rb +36 -0
  40. metadata +38 -23
  41. data/.travis.yml +0 -21
  42. data/CHANGELOG.md +0 -89
  43. data/Gemfile.old +0 -14
@@ -0,0 +1,10 @@
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: "../"
data/jbuilder.gemspec CHANGED
@@ -1,17 +1,31 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'jbuilder'
3
- s.version = '2.0.6'
4
- s.author = 'David Heinemeier Hansson'
5
- s.email = 'david@37signals.com'
3
+ s.version = '2.11.5'
4
+ s.authors = 'David Heinemeier Hansson'
5
+ s.email = 'david@basecamp.com'
6
6
  s.summary = 'Create JSON structures via a Builder-style DSL'
7
7
  s.homepage = 'https://github.com/rails/jbuilder'
8
8
  s.license = 'MIT'
9
9
 
10
- s.required_ruby_version = '>= 1.9.3'
10
+ s.required_ruby_version = '>= 2.2.2'
11
11
 
12
- s.add_dependency 'activesupport', '>= 3.0.0', '< 5'
13
- s.add_dependency 'multi_json', '~> 1.2'
12
+ s.add_dependency 'activesupport', '>= 5.0.0'
13
+ s.add_dependency 'actionview', '>= 5.0.0'
14
+
15
+ if RUBY_ENGINE == 'rbx'
16
+ s.add_development_dependency('racc')
17
+ s.add_development_dependency('json')
18
+ s.add_development_dependency('rubysl')
19
+ end
14
20
 
15
21
  s.files = `git ls-files`.split("\n")
16
22
  s.test_files = `git ls-files -- test/*`.split("\n")
23
+
24
+ s.metadata = {
25
+ "bug_tracker_uri" => "https://github.com/rails/jbuilder/issues",
26
+ "changelog_uri" => "https://github.com/rails/jbuilder/releases/tag/v#{s.version}",
27
+ "mailing_list_uri" => "https://discuss.rubyonrails.org/c/rubyonrails-talk",
28
+ "source_code_uri" => "https://github.com/rails/jbuilder/tree/v#{s.version}",
29
+ "rubygems_mfa_required" => "true",
30
+ }
17
31
  end
@@ -10,6 +10,8 @@ module Rails
10
10
 
11
11
  argument :attributes, type: :array, default: [], banner: 'field:type field:type'
12
12
 
13
+ class_option :timestamps, type: :boolean, default: true
14
+
13
15
  def create_root_folder
14
16
  path = File.join('app/views', controller_file_path)
15
17
  empty_directory path unless File.directory?(path)
@@ -20,6 +22,7 @@ module Rails
20
22
  filename = filename_with_extensions(view)
21
23
  template filename, File.join('app/views', controller_file_path, filename)
22
24
  end
25
+ template filename_with_extensions('partial'), File.join('app/views', controller_file_path, filename_with_extensions("_#{singular_table_name}"))
23
26
  end
24
27
 
25
28
 
@@ -32,8 +35,12 @@ module Rails
32
35
  [name, :json, :jbuilder] * '.'
33
36
  end
34
37
 
35
- def attributes_list_with_timestamps
36
- attributes_list(attributes_names + %w(created_at updated_at))
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
37
44
  end
38
45
 
39
46
  def attributes_list(attributes = attributes_names)
@@ -43,6 +50,10 @@ module Rails
43
50
 
44
51
  attributes.map { |a| ":#{a}"} * ', '
45
52
  end
53
+
54
+ def virtual_attributes
55
+ attributes.select {|name| name.respond_to?(:virtual?) && name.virtual? }
56
+ end
46
57
  end
47
58
  end
48
59
  end
@@ -4,9 +4,15 @@ require 'rails/generators/rails/scaffold_controller/scaffold_controller_generato
4
4
  module Rails
5
5
  module Generators
6
6
  class ScaffoldControllerGenerator
7
- source_root File.expand_path('../templates', __FILE__)
7
+ source_paths << File.expand_path('../templates', __FILE__)
8
8
 
9
- hook_for :jbuilder, default: true
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
10
16
  end
11
17
  end
12
- end
18
+ end
@@ -0,0 +1,63 @@
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
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
52
+ end
53
+
54
+ # Only allow a list of trusted parameters through.
55
+ def <%= "#{singular_table_name}_params" %>
56
+ <%- if attributes_names.empty? -%>
57
+ params.fetch(<%= ":#{singular_table_name}" %>, {})
58
+ <%- else -%>
59
+ params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
60
+ <%- end -%>
61
+ end
62
+ end
63
+ <% end -%>
@@ -1,19 +1,17 @@
1
1
  <% if namespaced? -%>
2
- require_dependency "<%= namespaced_file_path %>/application_controller"
2
+ require_dependency "<%= namespaced_path %>/application_controller"
3
3
 
4
4
  <% end -%>
5
5
  <% module_namespacing do -%>
6
6
  class <%= controller_class_name %>Controller < ApplicationController
7
- before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]
7
+ before_action :set_<%= singular_table_name %>, only: %i[ show edit update destroy ]
8
8
 
9
- # GET <%= route_url %>
10
- # GET <%= route_url %>.json
9
+ # GET <%= route_url %> or <%= route_url %>.json
11
10
  def index
12
11
  @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
13
12
  end
14
13
 
15
- # GET <%= route_url %>/1
16
- # GET <%= route_url %>/1.json
14
+ # GET <%= route_url %>/1 or <%= route_url %>/1.json
17
15
  def show
18
16
  end
19
17
 
@@ -26,42 +24,40 @@ class <%= controller_class_name %>Controller < ApplicationController
26
24
  def edit
27
25
  end
28
26
 
29
- # POST <%= route_url %>
30
- # POST <%= route_url %>.json
27
+ # POST <%= route_url %> or <%= route_url %>.json
31
28
  def create
32
29
  @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
33
30
 
34
31
  respond_to do |format|
35
32
  if @<%= orm_instance.save %>
36
- format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> }
33
+ format.html { redirect_to <%= show_helper %>, notice: <%= %("#{human_name} was successfully created.") %> }
37
34
  format.json { render :show, status: :created, location: <%= "@#{singular_table_name}" %> }
38
35
  else
39
- format.html { render :new }
36
+ format.html { render :new, status: :unprocessable_entity }
40
37
  format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
41
38
  end
42
39
  end
43
40
  end
44
41
 
45
- # PATCH/PUT <%= route_url %>/1
46
- # PATCH/PUT <%= route_url %>/1.json
42
+ # PATCH/PUT <%= route_url %>/1 or <%= route_url %>/1.json
47
43
  def update
48
44
  respond_to do |format|
49
45
  if @<%= orm_instance.update("#{singular_table_name}_params") %>
50
- format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> }
46
+ format.html { redirect_to <%= show_helper %>, notice: <%= %("#{human_name} was successfully updated.") %> }
51
47
  format.json { render :show, status: :ok, location: <%= "@#{singular_table_name}" %> }
52
48
  else
53
- format.html { render :edit }
49
+ format.html { render :edit, status: :unprocessable_entity }
54
50
  format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
55
51
  end
56
52
  end
57
53
  end
58
54
 
59
- # DELETE <%= route_url %>/1
60
- # DELETE <%= route_url %>/1.json
55
+ # DELETE <%= route_url %>/1 or <%= route_url %>/1.json
61
56
  def destroy
62
57
  @<%= orm_instance.destroy %>
58
+
63
59
  respond_to do |format|
64
- format.html { redirect_to <%= index_helper %>_url }
60
+ format.html { redirect_to <%= index_helper %>_url, notice: <%= %("#{human_name} was successfully destroyed.") %> }
65
61
  format.json { head :no_content }
66
62
  end
67
63
  end
@@ -72,12 +68,12 @@ class <%= controller_class_name %>Controller < ApplicationController
72
68
  @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
73
69
  end
74
70
 
75
- # Never trust parameters from the scary internet, only allow the white list through.
71
+ # Only allow a list of trusted parameters through.
76
72
  def <%= "#{singular_table_name}_params" %>
77
73
  <%- if attributes_names.empty? -%>
78
- params[<%= ":#{singular_table_name}" %>]
74
+ params.fetch(<%= ":#{singular_table_name}" %>, {})
79
75
  <%- else -%>
80
- params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
76
+ params.require(<%= ":#{singular_table_name}" %>).permit(<%= permitted_params %>)
81
77
  <%- end -%>
82
78
  end
83
79
  end
@@ -1,4 +1 @@
1
- json.array!(@<%= plural_table_name %>) do |<%= singular_table_name %>|
2
- json.extract! <%= singular_table_name %>, <%= attributes_list %>
3
- json.url <%= singular_table_name %>_url(<%= singular_table_name %>, format: :json)
4
- end
1
+ json.array! @<%= plural_table_name %>, partial: "<%= plural_table_name %>/<%= singular_table_name %>", as: :<%= singular_table_name %>
@@ -0,0 +1,16 @@
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 -%>
@@ -1 +1 @@
1
- json.extract! @<%= singular_table_name %>, <%= attributes_list_with_timestamps %>
1
+ json.partial! "<%= plural_table_name %>/<%= singular_table_name %>", <%= singular_table_name %>: @<%= singular_table_name %>
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,109 @@
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
+ end
@@ -1,4 +1,4 @@
1
- require 'jbuilder'
1
+ require 'jbuilder/jbuilder'
2
2
 
3
3
  dependency_tracker = false
4
4
 
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,7 @@
1
+ Jbuilder = Class.new(begin
2
+ require 'active_support/proxy_object'
3
+ ActiveSupport::ProxyObject
4
+ rescue LoadError
5
+ require 'active_support/basic_object'
6
+ ActiveSupport::BasicObject
7
+ end)