jbuilder 2.0.6 → 2.11.5

Sign up to get free protection for your applications and to get access to all the features.
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)