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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/MIT-LICENSE +20 -0
- data/README.md +72 -0
- data/Rakefile +20 -0
- data/lib/generators/graphiti/api_test_generator.rb +70 -0
- data/lib/generators/graphiti/generator_mixin.rb +45 -0
- data/lib/generators/graphiti/install_generator.rb +80 -0
- data/lib/generators/graphiti/resource_generator.rb +180 -0
- data/lib/generators/graphiti/resource_test_generator.rb +56 -0
- data/lib/generators/graphiti/templates/application_resource.rb.erb +15 -0
- data/lib/generators/graphiti/templates/controller.rb.erb +61 -0
- data/lib/generators/graphiti/templates/create_request_spec.rb.erb +35 -0
- data/lib/generators/graphiti/templates/destroy_request_spec.rb.erb +22 -0
- data/lib/generators/graphiti/templates/index_request_spec.rb.erb +22 -0
- data/lib/generators/graphiti/templates/resource.rb.erb +11 -0
- data/lib/generators/graphiti/templates/resource_reads_spec.rb.erb +78 -0
- data/lib/generators/graphiti/templates/resource_writes_spec.rb.erb +67 -0
- data/lib/generators/graphiti/templates/show_request_spec.rb.erb +21 -0
- data/lib/generators/graphiti/templates/update_request_spec.rb.erb +32 -0
- data/lib/graphiti-rails.rb +4 -0
- data/lib/graphiti/rails.rb +60 -0
- data/lib/graphiti/rails/context.rb +33 -0
- data/lib/graphiti/rails/debugging.rb +18 -0
- data/lib/graphiti/rails/exception_handlers.rb +83 -0
- data/lib/graphiti/rails/graphiti_errors_testing.rb +20 -0
- data/lib/graphiti/rails/railtie.rb +143 -0
- data/lib/graphiti/rails/responders.rb +21 -0
- data/lib/graphiti/rails/test_helpers.rb +7 -0
- data/lib/graphiti/rails/version.rb +7 -0
- data/lib/tasks/graphiti.rake +53 -0
- 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,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
|