graphiti-rails 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +10 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +72 -0
  5. data/Rakefile +20 -0
  6. data/lib/generators/graphiti/api_test_generator.rb +70 -0
  7. data/lib/generators/graphiti/generator_mixin.rb +45 -0
  8. data/lib/generators/graphiti/install_generator.rb +80 -0
  9. data/lib/generators/graphiti/resource_generator.rb +180 -0
  10. data/lib/generators/graphiti/resource_test_generator.rb +56 -0
  11. data/lib/generators/graphiti/templates/application_resource.rb.erb +15 -0
  12. data/lib/generators/graphiti/templates/controller.rb.erb +61 -0
  13. data/lib/generators/graphiti/templates/create_request_spec.rb.erb +35 -0
  14. data/lib/generators/graphiti/templates/destroy_request_spec.rb.erb +22 -0
  15. data/lib/generators/graphiti/templates/index_request_spec.rb.erb +22 -0
  16. data/lib/generators/graphiti/templates/resource.rb.erb +11 -0
  17. data/lib/generators/graphiti/templates/resource_reads_spec.rb.erb +78 -0
  18. data/lib/generators/graphiti/templates/resource_writes_spec.rb.erb +67 -0
  19. data/lib/generators/graphiti/templates/show_request_spec.rb.erb +21 -0
  20. data/lib/generators/graphiti/templates/update_request_spec.rb.erb +32 -0
  21. data/lib/graphiti-rails.rb +4 -0
  22. data/lib/graphiti/rails.rb +60 -0
  23. data/lib/graphiti/rails/context.rb +33 -0
  24. data/lib/graphiti/rails/debugging.rb +18 -0
  25. data/lib/graphiti/rails/exception_handlers.rb +83 -0
  26. data/lib/graphiti/rails/graphiti_errors_testing.rb +20 -0
  27. data/lib/graphiti/rails/railtie.rb +143 -0
  28. data/lib/graphiti/rails/responders.rb +21 -0
  29. data/lib/graphiti/rails/test_helpers.rb +7 -0
  30. data/lib/graphiti/rails/version.rb +7 -0
  31. data/lib/tasks/graphiti.rake +53 -0
  32. metadata +245 -0
@@ -0,0 +1,56 @@
1
+ require_relative "generator_mixin"
2
+
3
+ module Graphiti
4
+ class ResourceTestGenerator < ::Rails::Generators::Base
5
+ include GeneratorMixin
6
+
7
+ source_root File.expand_path("../templates", __FILE__)
8
+
9
+ argument :resource, type: :string
10
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
11
+ class_option :actions,
12
+ type: :array,
13
+ default: nil,
14
+ aliases: ["--actions", "-a"],
15
+ desc: 'Array of controller actions, e.g. "index show destroy"'
16
+
17
+ desc "Generates rspec request specs at spec/api"
18
+ def generate
19
+ generate_resource_specs
20
+ end
21
+
22
+ private
23
+
24
+ def var
25
+ dir.singularize
26
+ end
27
+
28
+ def dir
29
+ @resource.gsub("Resource", "").underscore.pluralize
30
+ end
31
+
32
+ def generate_resource_specs
33
+ if actions?("create", "update", "destroy")
34
+ to = "spec/resources/#{var}/writes_spec.rb"
35
+ template("resource_writes_spec.rb.erb", to)
36
+ end
37
+
38
+ if actions?("index", "show")
39
+ to = "spec/resources/#{var}/reads_spec.rb"
40
+ template("resource_reads_spec.rb.erb", to)
41
+ end
42
+ end
43
+
44
+ def resource_class
45
+ @resource.constantize
46
+ end
47
+
48
+ def type
49
+ resource_class.type
50
+ end
51
+
52
+ def model_class
53
+ resource_class.model
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ <%- unless omit_comments? -%>
2
+ # ApplicationResource is similar to ApplicationRecord - a base class that
3
+ # holds configuration/methods for subclasses.
4
+ # All Resources should inherit from ApplicationResource.
5
+ <%- end -%>
6
+ class ApplicationResource < Graphiti::Resource
7
+ <%- unless omit_comments? -%>
8
+ # Use the ActiveRecord Adapter for all subclasses.
9
+ # Subclasses can still override this default.
10
+ <%- end -%>
11
+ self.abstract_class = true
12
+ self.adapter = Graphiti::Adapters::ActiveRecord
13
+ self.base_url = Rails.application.routes.default_url_options[:host]
14
+ self.endpoint_namespace = '<%= api_namespace %>'
15
+ end
@@ -0,0 +1,61 @@
1
+ <% module_namespacing do -%>
2
+ class <%= model_klass.name.pluralize %>Controller < ApplicationController
3
+ <%- if actions?('index') -%>
4
+ def index
5
+ <%= file_name.pluralize %> = <%= resource_klass %>.all(params)
6
+ <%- if responders? -%>
7
+ respond_with(<%= file_name.pluralize %>)
8
+ <%- else -%>
9
+ render jsonapi: <%= file_name.pluralize %>
10
+ <%- end -%>
11
+ end
12
+ <%- end -%>
13
+ <%- if actions?('show') -%>
14
+
15
+ def show
16
+ <%= file_name %> = <%= resource_klass %>.find(params)
17
+ <%- if responders? -%>
18
+ respond_with(<%= file_name %>)
19
+ <%- else -%>
20
+ render jsonapi: <%= file_name %>
21
+ <%- end -%>
22
+ end
23
+ <%- end -%>
24
+ <%- if actions?('create') -%>
25
+
26
+ def create
27
+ <%= file_name %> = <%= resource_klass %>.build(params)
28
+
29
+ if <%= file_name %>.save
30
+ render jsonapi: <%= file_name %>, status: 201
31
+ else
32
+ render jsonapi_errors: <%= file_name %>
33
+ end
34
+ end
35
+ <%- end -%>
36
+ <%- if actions?('update') -%>
37
+
38
+ def update
39
+ <%= file_name %> = <%= resource_klass %>.find(params)
40
+
41
+ if <%= file_name %>.update_attributes
42
+ render jsonapi: <%= file_name %>
43
+ else
44
+ render jsonapi_errors: <%= file_name %>
45
+ end
46
+ end
47
+ <%- end -%>
48
+ <%- if actions?('destroy') -%>
49
+
50
+ def destroy
51
+ <%= file_name %> = <%= resource_klass %>.find(params)
52
+
53
+ if <%= file_name %>.destroy
54
+ render jsonapi: { meta: {} }, status: 200
55
+ else
56
+ render jsonapi_errors: <%= file_name %>
57
+ end
58
+ end
59
+ <%- end -%>
60
+ end
61
+ <% end -%>
@@ -0,0 +1,35 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe "<%= type %>#create", type: :request do
4
+ subject(:make_request) do
5
+ jsonapi_post "<%= api_namespace %>/<%= type %>", payload
6
+ end
7
+
8
+ describe 'basic create' do
9
+ let(:params) do
10
+ <%- if defined?(FactoryBot) -%>
11
+ attributes_for(:<%= type.to_s.singularize %>)
12
+ <%- else -%>
13
+ {
14
+ # ... your attrs here
15
+ }
16
+ <%- end -%>
17
+ end
18
+ let(:payload) do
19
+ {
20
+ data: {
21
+ type: '<%= type %>',
22
+ attributes: params
23
+ }
24
+ }
25
+ end
26
+
27
+ it 'works' do
28
+ expect(<%= resource_class %>).to receive(:build).and_call_original
29
+ expect {
30
+ make_request
31
+ expect(response.status).to eq(201), response.body
32
+ }.to change { <%= model_class %>.count }.by(1)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe "<%= type %>#destroy", type: :request do
4
+ subject(:make_request) do
5
+ jsonapi_delete "<%= api_namespace %>/<%= type %>/#{<%= var %>.id}"
6
+ end
7
+
8
+ describe 'basic destroy' do
9
+ let!(:<%= var %>) { create(:<%= var %>) }
10
+
11
+ it 'updates the resource' do
12
+ expect(<%= resource_class %>).to receive(:find).and_call_original
13
+ expect {
14
+ make_request
15
+ expect(response.status).to eq(200), response.body
16
+ }.to change { <%= model_class %>.count }.by(-1)
17
+ expect { <%= var %>.reload }
18
+ .to raise_error(ActiveRecord::RecordNotFound)
19
+ expect(json).to eq('meta' => {})
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe "<%= type %>#index", type: :request do
4
+ let(:params) { {} }
5
+
6
+ subject(:make_request) do
7
+ jsonapi_get "<%= api_namespace %>/<%= type %>", params: params
8
+ end
9
+
10
+ describe 'basic fetch' do
11
+ let!(:<%= var %>1) { create(:<%= var %>) }
12
+ let!(:<%= var %>2) { create(:<%= var %>) }
13
+
14
+ it 'works' do
15
+ expect(<%= resource_class %>).to receive(:all).and_call_original
16
+ make_request
17
+ expect(response.status).to eq(200), response.body
18
+ expect(d.map(&:jsonapi_type).uniq).to match_array(['<%= type %>'])
19
+ expect(d.map(&:id)).to match_array([<%= var %>1.id, <%= var %>2.id])
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Resource < ApplicationResource
3
+ <%- resource_attributes.each do |a| -%>
4
+ <%- if [:id, :created_at, :updated_at].include?(a.name.to_sym) -%>
5
+ attribute :<%= a.name %>, :<%= a.type %>, writable: false
6
+ <%- else -%>
7
+ attribute :<%= a.name %>, :<%= a.type %>
8
+ <%- end -%>
9
+ <%- end -%>
10
+ end
11
+ <% end -%>
@@ -0,0 +1,78 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe <%= resource_class %>, type: :resource do
4
+ describe 'serialization' do
5
+ let!(:<%= var %>) { create(:<%= var %>) }
6
+
7
+ it 'works' do
8
+ render
9
+ data = jsonapi_data[0]
10
+ expect(data.id).to eq(<%= var %>.id)
11
+ expect(data.jsonapi_type).to eq('<%= type %>')
12
+ <%- attributes.each do |a| -%>
13
+ <%- if [:created_at, :updated_at].include?(a.name.to_sym) -%>
14
+ expect(data.<%= a.name %>).to eq(datetime(<%= file_name %>.<%= a.name %>))
15
+ <%- else -%>
16
+ expect(data.<%= a.name %>).to eq(<%= file_name %>.<%= a.name %>)
17
+ <%- end -%>
18
+ <%- end -%>
19
+ end
20
+ end
21
+ <%- if actions?('index') -%>
22
+
23
+ describe 'filtering' do
24
+ let!(:<%= var %>1) { create(:<%= var %>) }
25
+ let!(:<%= var %>2) { create(:<%= var %>) }
26
+
27
+ context 'by id' do
28
+ before do
29
+ params[:filter] = { id: { eq: <%= var %>2.id } }
30
+ end
31
+
32
+ it 'works' do
33
+ render
34
+ expect(d.map(&:id)).to eq([<%= var %>2.id])
35
+ end
36
+ end
37
+ end
38
+
39
+ describe 'sorting' do
40
+ describe 'by id' do
41
+ let!(:<%= var %>1) { create(:<%= var %>) }
42
+ let!(:<%= var %>2) { create(:<%= var %>) }
43
+
44
+ context 'when ascending' do
45
+ before do
46
+ params[:sort] = 'id'
47
+ end
48
+
49
+ it 'works' do
50
+ render
51
+ expect(d.map(&:id)).to eq([
52
+ <%= var %>1.id,
53
+ <%= var %>2.id
54
+ ])
55
+ end
56
+ end
57
+
58
+ context 'when descending' do
59
+ before do
60
+ params[:sort] = '-id'
61
+ end
62
+
63
+ it 'works' do
64
+ render
65
+ expect(d.map(&:id)).to eq([
66
+ <%= var %>2.id,
67
+ <%= var %>1.id
68
+ ])
69
+ end
70
+ end
71
+ end
72
+ end
73
+ <%- end -%>
74
+
75
+ describe 'sideloading' do
76
+ # ... your tests ...
77
+ end
78
+ end
@@ -0,0 +1,67 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe <%= resource_class %>, type: :resource do
4
+ describe 'creating' do
5
+ let(:payload) do
6
+ {
7
+ data: {
8
+ type: '<%= type %>',
9
+ <%- if defined?(FactoryBot) -%>
10
+ attributes: attributes_for(:<%= type.to_s.singularize %>)
11
+ <%- else -%>
12
+ attributes: { }
13
+ <%- end -%>
14
+ }
15
+ }
16
+ end
17
+
18
+ let(:instance) do
19
+ <%= resource_class %>.build(payload)
20
+ end
21
+
22
+ it 'works' do
23
+ expect {
24
+ expect(instance.save).to eq(true), instance.errors.full_messages.to_sentence
25
+ }.to change { <%= model_class %>.count }.by(1)
26
+ end
27
+ end
28
+
29
+ describe 'updating' do
30
+ let!(:<%= var %>) { create(:<%= var %>) }
31
+
32
+ let(:payload) do
33
+ {
34
+ data: {
35
+ id: <%= var %>.id.to_s,
36
+ type: '<%= type %>',
37
+ attributes: { } # Todo!
38
+ }
39
+ }
40
+ end
41
+
42
+ let(:instance) do
43
+ <%= resource_class %>.find(payload)
44
+ end
45
+
46
+ xit 'works (add some attributes and enable this spec)' do
47
+ expect {
48
+ expect(instance.update_attributes).to eq(true)
49
+ }.to change { <%= var %>.reload.updated_at }
50
+ # .and change { <%= var %>.foo }.to('bar') <- example
51
+ end
52
+ end
53
+
54
+ describe 'destroying' do
55
+ let!(:<%= var %>) { create(:<%= var %>) }
56
+
57
+ let(:instance) do
58
+ <%= resource_class %>.find(id: <%= var %>.id)
59
+ end
60
+
61
+ it 'works' do
62
+ expect {
63
+ expect(instance.destroy).to eq(true)
64
+ }.to change { <%= model_class %>.count }.by(-1)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,21 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe "<%= type %>#show", type: :request do
4
+ let(:params) { {} }
5
+
6
+ subject(:make_request) do
7
+ jsonapi_get "<%= api_namespace %>/<%= type %>/#{<%= var %>.id}", params: params
8
+ end
9
+
10
+ describe 'basic fetch' do
11
+ let!(:<%= var %>) { create(:<%= var %>) }
12
+
13
+ it 'works' do
14
+ expect(<%= resource_class %>).to receive(:find).and_call_original
15
+ make_request
16
+ expect(response.status).to eq(200)
17
+ expect(d.jsonapi_type).to eq('<%= type %>')
18
+ expect(d.id).to eq(<%= var %>.id)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe "<%= type %>#update", type: :request do
4
+ subject(:make_request) do
5
+ jsonapi_put "<%= api_namespace %>/<%= type %>/#{<%= var %>.id}", payload
6
+ end
7
+
8
+ describe 'basic update' do
9
+ let!(:<%= var %>) { create(:<%= var %>) }
10
+
11
+ let(:payload) do
12
+ {
13
+ data: {
14
+ id: <%= var %>.id.to_s,
15
+ type: '<%= type %>',
16
+ attributes: {
17
+ # ... your attrs here
18
+ }
19
+ }
20
+ }
21
+ end
22
+
23
+ # Replace 'xit' with 'it' after adding attributes
24
+ xit 'updates the resource' do
25
+ expect(<%= resource_class %>).to receive(:find).and_call_original
26
+ expect {
27
+ make_request
28
+ expect(response.status).to eq(200), response.body
29
+ }.to change { <%= var %>.reload.attributes }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ require "graphiti"
2
+
3
+ # Load our version, not the built-in one
4
+ require_relative "graphiti/rails"
@@ -0,0 +1,60 @@
1
+ require 'rescue_registry'
2
+ require 'graphiti'
3
+ require 'rails'
4
+
5
+ module Graphiti
6
+ if defined?(Graphiti::Rails)
7
+ remove_const :Rails
8
+ end
9
+
10
+ # Rails integration for Graphiti. See {file:README.md} for more details.
11
+ module Rails
12
+ DEPRECATOR = ActiveSupport::Deprecation.new('1.0', 'graphiti-rails')
13
+
14
+ autoload :Context, "graphiti/rails/context"
15
+ autoload :Debugging, "graphiti/rails/debugging"
16
+ autoload :ExceptionHandler, "graphiti/rails/exception_handlers"
17
+ autoload :FallbackHandler, "graphiti/rails/exception_handlers"
18
+ autoload :GraphitiErrorsTesting, "graphiti/rails/graphiti_errors_testing"
19
+ autoload :InvalidRequestHandler, "graphiti/rails/exception_handlers"
20
+ autoload :Responders, "graphiti/rails/responders"
21
+ autoload :TestHelpers, "graphiti/rails/test_helpers"
22
+
23
+ def self.included(klass)
24
+ DEPRECATOR.deprecation_warning("Including Graphiti::Rails", "See https://www.graphiti.dev/guides/graphiti-rails-migration for help migrating to the new format")
25
+ require 'graphiti_errors'
26
+ klass.send(:include, GraphitiErrors)
27
+ end
28
+
29
+ # @!attribute self.handled_exception_formats
30
+ # A list of formats as symbols which will be handled by a GraphitiErrors::ExceptionHandler. See {Railtie}.
31
+ cattr_accessor :handled_exception_formats, default: []
32
+
33
+ # @!attribute self.respond_to_formats
34
+ # A list of formats as symbols which will be available for Graphiti::Rails::Responders. See {Railtie}.
35
+ cattr_accessor :respond_to_formats, default: []
36
+ end
37
+ end
38
+
39
+ ActiveSupport.on_load(:action_controller) do
40
+ include Graphiti::Rails::Context
41
+ include Graphiti::Rails::Debugging
42
+
43
+ # A global handler here is somewhat risky. However, we explicitly only handle JSON:API by default.
44
+ register_exception Exception, status: :passthrough, handler: Graphiti::Rails::FallbackHandler
45
+
46
+ register_exception Graphiti::Errors::InvalidRequest, status: 400, handler: Graphiti::Rails::InvalidRequestHandler
47
+ register_exception Graphiti::Errors::RecordNotFound, status: 404, handler: Graphiti::Rails::ExceptionHandler
48
+ register_exception Graphiti::Errors::RemoteWrite, status: 400, handler: Graphiti::Rails::ExceptionHandler
49
+ register_exception Graphiti::Errors::SingularSideload, status: 400, handler: Graphiti::Rails::ExceptionHandler
50
+ end
51
+
52
+ ActiveSupport.on_load(:active_record) do
53
+ require "graphiti/adapters/active_record"
54
+ end
55
+
56
+ require "graphiti/rails/railtie"
57
+
58
+ if defined?(GraphitiErrors) && Rails.respond_to?(:env) && Rails.env.test?
59
+ GraphitiErrors.singleton_class.prepend(Graphiti::Rails::GraphitiErrorsTesting)
60
+ end