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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0546b042d2d512c700094de8be871f95f65488ae003c508ce522bb881dfccb2e
4
+ data.tar.gz: 75dc08fb8add0c1a1eb6ef63c927ff83fdba84eb8cfa9d3b08197db4ee3dbae8
5
+ SHA512:
6
+ metadata.gz: e5d4a756cc7bd379e962830b1ff7171fdabae707799a2961e3df8d96746643a6061fe7b6cb1151aea0228fc1a34f42197e11c188a40976cbc0a6982ae09769c5
7
+ data.tar.gz: cce0b7aff8cc4301068c6c1e367abb094cc1bed28d7a5203736b4f9cf747082a73fd54438ec15d1e45cdc8e79b7a3a1f1ca9b42a933706e1612ba0c12120f701
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ No releases means no changes...
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Peter Wagenet
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,72 @@
1
+ # Graphiti::Rails
2
+
3
+ Graphiti::Rails provides robust Rails integration for Graphiti, following standard Rails conventions.
4
+
5
+ ## Usage
6
+ Out of the box, Graphiti::Rails requires no configuration!
7
+
8
+ ## Installation
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'graphiti-rails'
13
+ ```
14
+
15
+ ### Additional Setup
16
+
17
+ #### Debug Exception Format
18
+
19
+ If you're already running Rails in [API-only mode](https://guides.rubyonrails.org/api_app.html#changing-an-existing-application), there's no additional setup. Otherwise, we recommend the following in `config/application.rb`:
20
+
21
+ ```ruby
22
+ config.debug_exception_response_format = :api
23
+ ```
24
+
25
+ This will cause the [`ActionDispatch::DebugExceptions`][debug-exceptions] middleware to generate debug information in the requested content-type instead of as HTML only. In turn, this allows graphti-rails to generate more specific error messages for JSON API requests.
26
+
27
+ #### Handled Exception Formats
28
+
29
+ Since Rails doesn't correctly format exceptions for JSON:API requests, graphiti-rails intercepts these requests for proper rendering. If you'd like to use the GraphitiError handlers for other response types as well, you can add them in `config/application.rb`:
30
+
31
+ ```ruby
32
+ config.graphiti.handled_exception_formats += [:xml]
33
+ ```
34
+
35
+ ## Features
36
+
37
+ ### Exception Handling
38
+ By default, Rails does a few things to handle exceptions. We integrate into this handling to ensure behavior as close to the Rails defaults while still adding important conventions and additional information provide by Graphiti.
39
+
40
+ #### `rescue_from`
41
+
42
+ At the highest level, is [`rescue_from`][rescue-from] which allows you to handle an error at the controller level. This bypasses all default error handling in Rails, leaving it up to the developer to account for all scenarios. In the future, we would like to provide some APIs for default handling in these cases.
43
+
44
+ #### `ActionDispatch::DebugExceptions`
45
+
46
+ Next is [`ActionDispatch::DebugExceptions`][debug-exceptions]. This middleware logs exceptions and renders debugging information for local requests. We hook in here to log information in a proper format for JSON:API.
47
+
48
+ #### `ActionDispatch::ShowExceptions`
49
+
50
+ Last is [`ActionDispatch::ShowExceptions`][show-exceptions]. This middleware rescues any exception returned by the application and calls an exceptions app that will wrap it in a format for the end user. We wrap the exceptions app to ensure that JSON:API errors are always rendered in the standard format.
51
+
52
+ ### Context Wrapping
53
+ We wrap all requests in a Graphiti context pointing to the current controller instance. If you'd like to use a different context, overwrite the `graphiti_context` method in your controller.
54
+
55
+ For more information about Graphiti context, see the [Graphiti docs][context].
56
+
57
+ ### Debugging
58
+ We also provide hooks for Graphiti's built-in debugger. For more information see the [Graphiti docs][debugger].
59
+
60
+ ## Contributing
61
+ We'd love to have your help improving graphiti-rails. To chat with the team and other users, join the `#rails` channel on [Slack][slack].
62
+
63
+ ## License
64
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
65
+
66
+
67
+ [debug-exceptions]: https://api.rubyonrails.org/classes/ActionDispatch/DebugExceptions.html
68
+ [context]: https://www.graphiti.dev/guides/concepts/resources#context
69
+ [debugger]: https://www.graphiti.dev/guides/concepts/debugging#debugger
70
+ [rescue-from]: https://api.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html#method-i-rescue_from
71
+ [show-exceptions]: https://api.rubyonrails.org/classes/ActionDispatch/ShowExceptions.html
72
+ [slack]: https://join.slack.com/t/graphiti-api/shared_invite/enQtMjkyMTA3MDgxNTQzLWVkMDM3NTlmNTIwODY2YWFkMGNiNzUzZGMzOTY3YmNmZjBhYzIyZWZlZTk4YmI1YTI0Y2M0OTZmZGYwN2QxZjg
@@ -0,0 +1,20 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'yard'
8
+ require 'rspec/core/rake_task'
9
+ require 'bundler/gem_tasks'
10
+
11
+ YARD::Rake::YardocTask.new
12
+
13
+ RSpec::Core::RakeTask.new(:spec) do |t|
14
+ t.rspec_opts = "--order random"
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
18
+ load "rails/tasks/engine.rake"
19
+
20
+ task default: :spec
@@ -0,0 +1,70 @@
1
+ require_relative "generator_mixin"
2
+
3
+ module Graphiti
4
+ class ApiTestGenerator < ::Rails::Generators::Base
5
+ include GeneratorMixin
6
+
7
+ source_root File.expand_path("../templates", __FILE__)
8
+
9
+ argument :resource, type: :string
10
+ class_option :actions,
11
+ type: :array,
12
+ default: nil,
13
+ aliases: ["--actions", "-a"],
14
+ desc: 'Array of controller actions, e.g. "index show destroy"'
15
+
16
+ desc "Generates rspec request specs at spec/api"
17
+ def generate
18
+ generate_api_specs
19
+ end
20
+
21
+ private
22
+
23
+ def var
24
+ dir.singularize
25
+ end
26
+
27
+ def dir
28
+ @resource.gsub("Resource", "").underscore.pluralize
29
+ end
30
+
31
+ def generate_api_specs
32
+ if actions?("index")
33
+ to = File.join("spec", ApplicationResource.endpoint_namespace, dir, "index_spec.rb")
34
+ template("index_request_spec.rb.erb", to)
35
+ end
36
+
37
+ if actions?("show")
38
+ to = File.join("spec", ApplicationResource.endpoint_namespace, dir, "show_spec.rb")
39
+ template("show_request_spec.rb.erb", to)
40
+ end
41
+
42
+ if actions?("create")
43
+ to = File.join("spec", ApplicationResource.endpoint_namespace, dir, "create_spec.rb")
44
+ template("create_request_spec.rb.erb", to)
45
+ end
46
+
47
+ if actions?("update")
48
+ to = File.join("spec", ApplicationResource.endpoint_namespace, dir, "update_spec.rb")
49
+ template("update_request_spec.rb.erb", to)
50
+ end
51
+
52
+ if actions?("destroy")
53
+ to = File.join("spec", ApplicationResource.endpoint_namespace, dir, "destroy_spec.rb")
54
+ template("destroy_request_spec.rb.erb", to)
55
+ end
56
+ end
57
+
58
+ def resource_class
59
+ @resource.constantize
60
+ end
61
+
62
+ def type
63
+ resource_class.type
64
+ end
65
+
66
+ def model_class
67
+ resource_class.model
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,45 @@
1
+ module Graphiti
2
+ module GeneratorMixin
3
+ def prompt(header: nil, description: nil, default: nil)
4
+ say(set_color("\n#{header}", :magenta, :bold)) if header
5
+ say("\n#{description}") if description
6
+ answer = ask(set_color("\n(default: #{default}):", :magenta, :bold))
7
+ answer = default if answer.blank? && default != "nil"
8
+ say(set_color("\nGot it!\n", :white, :bold))
9
+ answer
10
+ end
11
+
12
+ def api_namespace
13
+ @api_namespace ||= begin
14
+ ns = graphiti_config["namespace"]
15
+
16
+ if ns.blank?
17
+ ns = prompt \
18
+ header: "What is your API namespace?",
19
+ description: "This will be used as a route prefix, e.g. if you want the route '/books_api/v1/authors' your namespace would be '/books_api/v1'",
20
+ default: "/api/v1"
21
+ update_config!("namespace" => ns)
22
+ end
23
+
24
+ ns
25
+ end
26
+ end
27
+
28
+ def actions
29
+ @options["actions"] || %w[index show create update destroy]
30
+ end
31
+
32
+ def actions?(*methods)
33
+ methods.any? { |m| actions.include?(m) }
34
+ end
35
+
36
+ def graphiti_config
37
+ File.exist?(".graphiticfg.yml") ? YAML.load_file(".graphiticfg.yml") : {}
38
+ end
39
+
40
+ def update_config!(attrs)
41
+ config = graphiti_config.merge(attrs)
42
+ File.open(".graphiticfg.yml", "w") { |f| f.write(config.to_yaml) }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,80 @@
1
+ require_relative "generator_mixin"
2
+
3
+ module Graphiti
4
+ class InstallGenerator < ::Rails::Generators::Base
5
+ include GeneratorMixin
6
+
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ class_option :'omit-comments',
10
+ type: :boolean,
11
+ default: false,
12
+ aliases: ["-c"],
13
+ desc: "Generate without documentation comments"
14
+
15
+ desc "This generator boostraps graphiti"
16
+ def install
17
+ to = File.join("app/resources", "application_resource.rb")
18
+ template("application_resource.rb.erb", to)
19
+
20
+ inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::API\n" do
21
+ app_controller_code
22
+ end
23
+
24
+ inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::Base\n" do
25
+ app_controller_code
26
+ end
27
+
28
+ inject_into_file "config/application.rb", after: "Rails::Application\n" do
29
+ <<-'RUBY'
30
+ # In order for Graphiti to generate links, you need to set the routes host.
31
+ # When not explicitly set, via the HOST env var, this will fall back to
32
+ # the rails server settings.
33
+ # Rails::Server is not defined in console or rake tasks, so this will only
34
+ # use those defaults when they are available.
35
+ routes.default_url_options[:host] = ENV.fetch('HOST') do
36
+ if defined?(Rails::Server)
37
+ argv_options = Rails::Server::Options.new.parse!(ARGV)
38
+ "http://#{argv_options[:Host]}:#{argv_options[:Port]}"
39
+ end
40
+ end
41
+ RUBY
42
+ end
43
+
44
+ inject_into_file "spec/rails_helper.rb", after: /RSpec.configure.+^end$/m do
45
+ "\n\nGraphitiSpecHelpers::RSpec.schema!"
46
+ end
47
+
48
+ insert_into_file "config/routes.rb", after: "Rails.application.routes.draw do\n" do
49
+ if defined?(VandalUi)
50
+ <<-STR
51
+ scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
52
+ mount VandalUi::Engine, at: '/vandal'
53
+ # your routes go here
54
+ end
55
+ STR
56
+ else
57
+ <<-STR
58
+ scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
59
+ # your routes go here
60
+ end
61
+ STR
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def omit_comments?
69
+ @options["omit-comments"]
70
+ end
71
+
72
+ def app_controller_code
73
+ str = ""
74
+ if defined?(::Responders)
75
+ str << " include Graphiti::Rails::Responders\n"
76
+ end
77
+ str
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,180 @@
1
+ require_relative "generator_mixin"
2
+
3
+ module Graphiti
4
+ class ResourceGenerator < ::Rails::Generators::NamedBase
5
+ include GeneratorMixin
6
+
7
+ source_root File.expand_path("../templates", __FILE__)
8
+
9
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
10
+
11
+ class_option :'omit-comments',
12
+ type: :boolean,
13
+ default: false,
14
+ aliases: ["--omit-comments", "-c"],
15
+ desc: "Generate without documentation comments"
16
+
17
+ class_option :actions,
18
+ type: :array,
19
+ default: nil,
20
+ aliases: ["--actions", "-a"],
21
+ desc: 'Array of controller actions to support, e.g. "index show destroy"'
22
+
23
+ class_option :'attributes-from',
24
+ banner: "Model",
25
+ type: :string,
26
+ aliases: ["--model", "-m"],
27
+ desc: "Specify to use attributes from a particular model"
28
+
29
+ desc "This generator creates a resource file at app/resources, as well as corresponding controller/specs/route/etc"
30
+ def generate_all
31
+ generate_model
32
+ generate_controller
33
+ generate_application_resource unless application_resource_defined?
34
+ generate_route
35
+ generate_resource
36
+ generate_resource_specs
37
+ generate_api_specs
38
+ end
39
+
40
+ private
41
+
42
+ class ModelAction
43
+ attr_reader :class_name
44
+ def initialize(class_name)
45
+ @class_name = class_name
46
+ end
47
+
48
+ def invoke!
49
+ unless class_name.safe_constantize
50
+ raise "You must define a #{class_name} model before generating the corresponding resource."
51
+ end
52
+ end
53
+
54
+ def revoke!
55
+ # Do nothing on destroy
56
+ end
57
+ end
58
+
59
+ def generate_model
60
+ action(ModelAction.new(class_name))
61
+ end
62
+
63
+ def omit_comments?
64
+ @options["omit-comments"]
65
+ end
66
+
67
+ def attributes_class
68
+ return @attributes_class if @attributes_class
69
+
70
+ case @options["attributes-from"]
71
+ # thor will set the value to the key if no value is specified
72
+ when "attributes-from"
73
+ klass = class_name
74
+ when :kind_of?, String
75
+ klass = @options["attributes-from"].classify
76
+ else
77
+ # return nil if attributes-from isn't set or has an invalid value
78
+ return
79
+ end
80
+ begin
81
+ @attributes_class = klass.safe_constantize
82
+ rescue NameError
83
+ raise NameError, "attributes-from #{klass.inspect} does not exist."
84
+ end
85
+ end
86
+
87
+ ##
88
+ # Generates a list of OpenStruct(:name, :type) objects that map to
89
+ # the +attributes_class+ columns.
90
+ ##
91
+ def default_attributes
92
+ unless attributes_class.is_a?(Class) && attributes_class <= ApplicationRecord
93
+ raise "Unable to set #{self} default_attributes from #{attributes_class}. #{attributes_class} must be a kind of ApplicationRecord"
94
+ end
95
+ if attributes_class.table_exists?
96
+ return attributes_class.columns.map do |c|
97
+ OpenStruct.new({name: c.name.to_sym, type: c.type})
98
+ end
99
+ else
100
+ raise "#{attributes_class} table must exist. Please run migrations."
101
+ end
102
+ end
103
+
104
+ def resource_attributes
105
+ # set a temporary variable because overriding attributes causes
106
+ # weird behavior when the generator is run. It will override
107
+ # everytime regardless of the conditional.
108
+ if !attributes_class.nil?
109
+ default_attributes
110
+ else
111
+ attributes
112
+ end
113
+ end
114
+
115
+ def responders?
116
+ defined?(::Responders)
117
+ end
118
+
119
+ def generate_controller
120
+ to = File.join("app/controllers", class_path, "#{file_name.pluralize}_controller.rb")
121
+ template("controller.rb.erb", to)
122
+ end
123
+
124
+ def generate_application_resource
125
+ to = File.join("app/resources", class_path, "application_resource.rb")
126
+ template("application_resource.rb.erb", to)
127
+ require "#{::Rails.root}/#{to}"
128
+ end
129
+
130
+ def application_resource_defined?
131
+ "ApplicationResource".safe_constantize.present?
132
+ end
133
+
134
+ def generate_route
135
+ # Rails 5.2 adds `plural_route_name`, fallback to `plural_table_name`
136
+ plural_name = try(:plural_route_name) || plural_table_name
137
+
138
+ code = "resources :#{plural_name}"
139
+ code << %(, only: [#{actions.map { |a| ":#{a}" }.join(", ")}]) if actions.length < 5
140
+ code << "\n"
141
+ inject_into_file "config/routes.rb", after: /ApplicationResource.*$\n/ do
142
+ indent(code, 4)
143
+ end
144
+ end
145
+
146
+ def generate_resource_specs
147
+ opts = {}
148
+ opts[:actions] = @options[:actions] if @options[:actions]
149
+ invoke "graphiti:resource_test", [resource_klass], opts
150
+ end
151
+
152
+ def generate_api_specs
153
+ opts = {}
154
+ opts[:actions] = @options[:actions] if @options[:actions]
155
+ invoke "graphiti:api_test", [resource_klass], opts
156
+ end
157
+
158
+ def generate_resource
159
+ to = File.join("app/resources", class_path, "#{file_name}_resource.rb")
160
+ template("resource.rb.erb", to)
161
+ require "#{::Rails.root}/#{to}" if create?
162
+ end
163
+
164
+ def create?
165
+ behavior == :invoke
166
+ end
167
+
168
+ def model_klass
169
+ class_name.safe_constantize
170
+ end
171
+
172
+ def resource_klass
173
+ "#{model_klass}Resource"
174
+ end
175
+
176
+ def type
177
+ model_klass.name.underscore.pluralize
178
+ end
179
+ end
180
+ end