graphiti-rails 0.2.0

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 (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