jets-responders 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3d3a01e41bd6fe4be4e5302b20b73fe91585aab6e92d72a285947d2ce561b94f
4
+ data.tar.gz: 834e78db1644e8c86dc959e3966091b9e0bcf3f128812de98bb7d4b0d87ad1dd
5
+ SHA512:
6
+ metadata.gz: 4be0dd3874fddd4bfe1ac5ec9e486d41c621e6da244e71b1aef34c29aed8911f9d7d6d97a0f64f99aad7737d5c78a8cb8693ae65412524753af05a2f68a4d524
7
+ data.tar.gz: ffb85dc800ebd0ab1c86238ec46e5312a01f4f70271eb12104e9699a358b66c4660032fed771be0f5d16d6e3b3348cd6101fecc63cb7589344788d9bffccc845
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ /.bundle
4
+ /.config
5
+ /.yardoc
6
+ /_yardoc
7
+ /coverage
8
+ /doc/
9
+ /Gemfile.lock
10
+ /InstalledFiles
11
+ /lib/bundler/man
12
+ /pkg
13
+ /rdoc
14
+ /spec/reports
15
+ /test/tmp
16
+ /test/version_tmp
17
+ /tmp
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
+
6
+ ## [0.1.0]
7
+ - Initial release.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem dependencies in gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,19 @@
1
+ guard "bundler", cmd: "bundle" do
2
+ watch("Gemfile")
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard :rspec, cmd: "bundle exec rspec" do
7
+ require "guard/rspec/dsl"
8
+ dsl = Guard::RSpec::Dsl.new(self)
9
+
10
+ # RSpec files
11
+ rspec = dsl.rspec
12
+ watch(rspec.spec_helper) { rspec.spec_dir }
13
+ watch(rspec.spec_support) { rspec.spec_dir }
14
+ watch(rspec.spec_files)
15
+
16
+ # Ruby files
17
+ ruby = dsl.ruby
18
+ dsl.watch_spec_files_for(ruby.lib_files)
19
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Tung Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # Responders
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/jets-responders.png)](http://badge.fury.io/rb/jets-responders)
4
+
5
+ [![BoltOps Badge](https://img.boltops.com/boltops/badges/boltops-badge.png)](https://www.boltops.com)
6
+
7
+ [![BoltOps Learn Badge](https://img.boltops.com/boltops-learn/boltops-learn.png)](https://learn.boltops.com)
8
+
9
+ A set of responders modules to dry up your Jets app.
10
+
11
+ This is a humble port of the original [responders](https://github.com/heartcombo/responders) to support the [Ruby on Jets](https://rubyonjets.com/) framework. A big thanks to the original authors of responders.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "responders/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jets-responders"
8
+ spec.version = Responders::VERSION
9
+ spec.authors = ["Tung Nguyen"]
10
+ spec.email = ["tung@boltops.com"]
11
+ spec.summary = "A set of Jets responders to dry up your application"
12
+ spec.homepage = "https://github.com/rubyonjets/jets-responders"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = File.directory?('.git') ? `git ls-files`.split($/) : Dir.glob("**/*")
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "memoist"
23
+ spec.add_dependency "rainbow"
24
+ spec.add_dependency "zeitwerk"
25
+
26
+ spec.add_development_dependency "bundler"
27
+ spec.add_development_dependency "byebug"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "rspec"
30
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/extract_options"
4
+ require "action_controller/metal/mime_responds"
5
+
6
+ module Jets::Controller # :nodoc:
7
+ module RespondWith
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :responder, :mimes_for_respond_to
12
+ self.responder = Jets::Controller::Responder
13
+ clear_respond_to
14
+ end
15
+
16
+ module ClassMethods
17
+ # Defines mime types that are rendered by default when invoking
18
+ # <tt>respond_with</tt>.
19
+ #
20
+ # respond_to :html, :xml, :json
21
+ #
22
+ # Specifies that all actions in the controller respond to requests
23
+ # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>.
24
+ #
25
+ # To specify on per-action basis, use <tt>:only</tt> and
26
+ # <tt>:except</tt> with an array of actions or a single action:
27
+ #
28
+ # respond_to :html
29
+ # respond_to :xml, :json, except: [ :edit ]
30
+ #
31
+ # This specifies that all actions respond to <tt>:html</tt>
32
+ # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and
33
+ # <tt>:json</tt>.
34
+ #
35
+ # respond_to :json, only: :create
36
+ #
37
+ # This specifies that the <tt>:create</tt> action and no other responds
38
+ # to <tt>:json</tt>.
39
+ def respond_to(*mimes)
40
+ options = mimes.extract_options!
41
+
42
+ only_actions = Array(options.delete(:only)).map(&:to_sym)
43
+ except_actions = Array(options.delete(:except)).map(&:to_sym)
44
+
45
+ hash = mimes_for_respond_to.dup
46
+ mimes.each do |mime|
47
+ mime = mime.to_sym
48
+ hash[mime] = {}
49
+ hash[mime][:only] = only_actions unless only_actions.empty?
50
+ hash[mime][:except] = except_actions unless except_actions.empty?
51
+ end
52
+ self.mimes_for_respond_to = hash.freeze
53
+ end
54
+
55
+ # Clear all mime types in <tt>respond_to</tt>.
56
+ #
57
+ def clear_respond_to
58
+ self.mimes_for_respond_to = Hash.new.freeze
59
+ end
60
+ end
61
+
62
+ # For a given controller action, respond_with generates an appropriate
63
+ # response based on the mime-type requested by the client.
64
+ #
65
+ # If the method is called with just a resource, as in this example -
66
+ #
67
+ # class PeopleController < ApplicationController
68
+ # respond_to :html, :xml, :json
69
+ #
70
+ # def index
71
+ # @people = Person.all
72
+ # respond_with @people
73
+ # end
74
+ # end
75
+ #
76
+ # then the mime-type of the response is typically selected based on the
77
+ # request's Accept header and the set of available formats declared
78
+ # by previous calls to the controller's class method +respond_to+. Alternatively
79
+ # the mime-type can be selected by explicitly setting <tt>request.format</tt> in
80
+ # the controller.
81
+ #
82
+ # If an acceptable format is not identified, the application returns a
83
+ # '406 - not acceptable' status. Otherwise, the default response is to render
84
+ # a template named after the current action and the selected format,
85
+ # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior
86
+ # depends on the selected format:
87
+ #
88
+ # * for an html response - if the request method is +get+, an exception
89
+ # is raised but for other requests such as +post+ the response
90
+ # depends on whether the resource has any validation errors (i.e.
91
+ # assuming that an attempt has been made to save the resource,
92
+ # e.g. by a +create+ action) -
93
+ # 1. If there are no errors, i.e. the resource
94
+ # was saved successfully, the response +redirect+'s to the resource
95
+ # i.e. its +show+ action.
96
+ # 2. If there are validation errors, the response
97
+ # renders a default action, which is <tt>:new</tt> for a
98
+ # +post+ request or <tt>:edit</tt> for +patch+ or +put+,
99
+ # and the status is set based on the configured `error_status`.
100
+ # (defaults to `422 Unprocessable Entity` on new apps,
101
+ # `200 OK` for compatibility reasons on old apps.)
102
+ # Thus an example like this -
103
+ #
104
+ # respond_to :html, :xml
105
+ #
106
+ # def create
107
+ # @user = User.new(params[:user])
108
+ # flash[:notice] = 'User was successfully created.' if @user.save
109
+ # respond_with(@user)
110
+ # end
111
+ #
112
+ # is equivalent, in the absence of <tt>create.html.erb</tt>, to -
113
+ #
114
+ # def create
115
+ # @user = User.new(params[:user])
116
+ # respond_to do |format|
117
+ # if @user.save
118
+ # flash[:notice] = 'User was successfully created.'
119
+ # format.html { redirect_to(@user) }
120
+ # format.xml { render xml: @user }
121
+ # else
122
+ # format.html { render action: "new", status: :unprocessable_entity }
123
+ # format.xml { render xml: @user, status: :unprocessable_entity }
124
+ # end
125
+ # end
126
+ # end
127
+ #
128
+ # * for a JavaScript request - if the template isn't found, an exception is
129
+ # raised.
130
+ # * for other requests - i.e. data formats such as xml, json, csv etc, if
131
+ # the resource passed to +respond_with+ responds to <code>to_<format></code>,
132
+ # the method attempts to render the resource in the requested format
133
+ # directly, e.g. for an xml request, the response is equivalent to calling
134
+ # <code>render xml: resource</code>.
135
+ #
136
+ # === Nested resources
137
+ #
138
+ # As outlined above, the +resources+ argument passed to +respond_with+
139
+ # can play two roles. It can be used to generate the redirect url
140
+ # for successful html requests (e.g. for +create+ actions when
141
+ # no template exists), while for formats other than html and JavaScript
142
+ # it is the object that gets rendered, by being converted directly to the
143
+ # required format (again assuming no template exists).
144
+ #
145
+ # For redirecting successful html requests, +respond_with+ also supports
146
+ # the use of nested resources, which are supplied in the same way as
147
+ # in <code>form_for</code> and <code>polymorphic_url</code>. For example -
148
+ #
149
+ # def create
150
+ # @project = Project.find(params[:project_id])
151
+ # @task = @project.comments.build(params[:task])
152
+ # flash[:notice] = 'Task was successfully created.' if @task.save
153
+ # respond_with(@project, @task)
154
+ # end
155
+ #
156
+ # This would cause +respond_with+ to redirect to <code>project_task_url</code>
157
+ # instead of <code>task_url</code>. For request formats other than html or
158
+ # JavaScript, if multiple resources are passed in this way, it is the last
159
+ # one specified that is rendered.
160
+ #
161
+ # === Customizing response behavior
162
+ #
163
+ # Like +respond_to+, +respond_with+ may also be called with a block that
164
+ # can be used to overwrite any of the default responses, e.g. -
165
+ #
166
+ # def create
167
+ # @user = User.new(params[:user])
168
+ # flash[:notice] = "User was successfully created." if @user.save
169
+ #
170
+ # respond_with(@user) do |format|
171
+ # format.html { render }
172
+ # end
173
+ # end
174
+ #
175
+ # The argument passed to the block is an ActionController::MimeResponds::Collector
176
+ # object which stores the responses for the formats defined within the
177
+ # block. Note that formats with responses defined explicitly in this way
178
+ # do not have to first be declared using the class method +respond_to+.
179
+ #
180
+ # Also, a hash passed to +respond_with+ immediately after the specified
181
+ # resource(s) is interpreted as a set of options relevant to all
182
+ # formats. Any option accepted by +render+ can be used, e.g.
183
+ #
184
+ # respond_with @people, status: 200
185
+ #
186
+ # However, note that these options are ignored after an unsuccessful attempt
187
+ # to save a resource, e.g. when automatically rendering <tt>:new</tt>
188
+ # after a post request.
189
+ #
190
+ # Three additional options are relevant specifically to +respond_with+ -
191
+ # 1. <tt>:location</tt> - overwrites the default redirect location used after
192
+ # a successful html +post+ request.
193
+ # 2. <tt>:action</tt> - overwrites the default render action used after an
194
+ # unsuccessful html +post+ request.
195
+ # 3. <tt>:render</tt> - allows to pass any options directly to the <tt>:render<tt/>
196
+ # call after unsuccessful html +post+ request. Useful if for example you
197
+ # need to render a template which is outside of controller's path or you
198
+ # want to override the default http <tt>:status</tt> code, e.g.
199
+ #
200
+ # respond_with(resource, render: { template: 'path/to/template', status: 418 })
201
+ def respond_with(*resources, &block)
202
+ if self.class.mimes_for_respond_to.empty?
203
+ raise "In order to use respond_with, first you need to declare the " \
204
+ "formats your controller responds to in the class level."
205
+ end
206
+
207
+ mimes = collect_mimes_from_class_level
208
+ collector = ActionController::MimeResponds::Collector.new(mimes, request.variant)
209
+ block.call(collector) if block_given?
210
+
211
+ if format = collector.negotiate_format(request)
212
+ _process_format(format)
213
+ options = resources.size == 1 ? {} : resources.extract_options!
214
+ options = options.clone
215
+
216
+ options[:default_response] = collector.response
217
+ (options.delete(:responder) || self.class.responder).call(self, resources, options)
218
+ else
219
+ raise ActionController::UnknownFormat
220
+ end
221
+ end
222
+
223
+ protected
224
+
225
+ # Before action callback that can be used to prevent requests that do not
226
+ # match the mime types defined through <tt>respond_to</tt> from being executed.
227
+ #
228
+ # class PeopleController < ApplicationController
229
+ # respond_to :html, :xml, :json
230
+ #
231
+ # before_action :verify_requested_format!
232
+ # end
233
+ def verify_requested_format!
234
+ mimes = collect_mimes_from_class_level
235
+ collector = ActionController::MimeResponds::Collector.new(mimes, request.variant)
236
+
237
+ unless collector.negotiate_format(request)
238
+ raise ActionController::UnknownFormat
239
+ end
240
+ end
241
+
242
+ alias :verify_request_format! :verify_requested_format!
243
+
244
+ # Collect mimes declared in the class method respond_to valid for the
245
+ # current action.
246
+ def collect_mimes_from_class_level # :nodoc:
247
+ action = action_name.to_sym
248
+
249
+ self.class.mimes_for_respond_to.keys.select do |mime|
250
+ config = self.class.mimes_for_respond_to[mime]
251
+
252
+ if config[:except]
253
+ !config[:except].include?(action)
254
+ elsif config[:only]
255
+ config[:only].include?(action)
256
+ else
257
+ true
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/json"
4
+
5
+ module Jets::Controller # :nodoc:
6
+ # Responsible for exposing a resource to different mime requests,
7
+ # usually depending on the HTTP verb. The responder is triggered when
8
+ # <code>respond_with</code> is called. The simplest case to study is a GET request:
9
+ #
10
+ # class PeopleController < ApplicationController
11
+ # respond_to :html, :xml, :json
12
+ #
13
+ # def index
14
+ # @people = Person.all
15
+ # respond_with(@people)
16
+ # end
17
+ # end
18
+ #
19
+ # When a request comes in, for example for an XML response, three steps happen:
20
+ #
21
+ # 1) the responder searches for a template at people/index.xml;
22
+ #
23
+ # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource;
24
+ #
25
+ # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
26
+ #
27
+ # === Built-in HTTP verb semantics
28
+ #
29
+ # The default \Rails responder holds semantics for each HTTP verb. Depending on the
30
+ # content type, verb and the resource status, it will behave differently.
31
+ #
32
+ # Using \Rails default responder, a POST request for creating an object could
33
+ # be written as:
34
+ #
35
+ # def create
36
+ # @user = User.new(params[:user])
37
+ # flash[:notice] = 'User was successfully created.' if @user.save
38
+ # respond_with(@user)
39
+ # end
40
+ #
41
+ # Which is exactly the same as:
42
+ #
43
+ # def create
44
+ # @user = User.new(params[:user])
45
+ #
46
+ # respond_to do |format|
47
+ # if @user.save
48
+ # flash[:notice] = 'User was successfully created.'
49
+ # format.html { redirect_to(@user) }
50
+ # format.xml { render xml: @user, status: :created, location: @user }
51
+ # else
52
+ # format.html { render action: "new", status: :unprocessable_entity }
53
+ # format.xml { render xml: @user.errors, status: :unprocessable_entity }
54
+ # end
55
+ # end
56
+ # end
57
+ #
58
+ # The same happens for PATCH/PUT and DELETE requests.
59
+ #
60
+ # === Nested resources
61
+ #
62
+ # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>.
63
+ # Consider the project has many tasks example. The create action for
64
+ # TasksController would be like:
65
+ #
66
+ # def create
67
+ # @project = Project.find(params[:project_id])
68
+ # @task = @project.tasks.build(params[:task])
69
+ # flash[:notice] = 'Task was successfully created.' if @task.save
70
+ # respond_with(@project, @task)
71
+ # end
72
+ #
73
+ # Giving several resources ensures that the responder will redirect to
74
+ # <code>project_task_url</code> instead of <code>task_url</code>.
75
+ #
76
+ # Namespaced and singleton resources require a symbol to be given, as in
77
+ # polymorphic urls. If a project has one manager which has many tasks, it
78
+ # should be invoked as:
79
+ #
80
+ # respond_with(@project, :manager, @task)
81
+ #
82
+ # Note that if you give an array, it will be treated as a collection,
83
+ # so the following is not equivalent:
84
+ #
85
+ # respond_with [@project, :manager, @task]
86
+ #
87
+ # === Custom options
88
+ #
89
+ # <code>respond_with</code> also allows you to pass options that are forwarded
90
+ # to the underlying render call. Those options are only applied for success
91
+ # scenarios. For instance, you can do the following in the create method above:
92
+ #
93
+ # def create
94
+ # @project = Project.find(params[:project_id])
95
+ # @task = @project.tasks.build(params[:task])
96
+ # flash[:notice] = 'Task was successfully created.' if @task.save
97
+ # respond_with(@project, @task, status: 201)
98
+ # end
99
+ #
100
+ # This will return status 201 if the task was saved successfully. If not,
101
+ # it will simply ignore the given options and return status 422 and the
102
+ # resource errors. You can also override the location to redirect to:
103
+ #
104
+ # respond_with(@project, location: root_path)
105
+ #
106
+ # To customize the failure scenario, you can pass a block to
107
+ # <code>respond_with</code>:
108
+ #
109
+ # def create
110
+ # @project = Project.find(params[:project_id])
111
+ # @task = @project.tasks.build(params[:task])
112
+ # respond_with(@project, @task, status: 201) do |format|
113
+ # if @task.save
114
+ # flash[:notice] = 'Task was successfully created.'
115
+ # else
116
+ # format.html { render "some_special_template", status: :unprocessable_entity }
117
+ # end
118
+ # end
119
+ # end
120
+ #
121
+ # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>.
122
+ class Responder
123
+ class_attribute :error_status, default: :ok, instance_writer: false, instance_predicate: false
124
+ class_attribute :redirect_status, default: :found, instance_writer: false, instance_predicate: false
125
+
126
+ attr_reader :controller, :request, :format, :resource, :resources, :options
127
+
128
+ DEFAULT_ACTIONS_FOR_VERBS = {
129
+ post: :new,
130
+ patch: :edit,
131
+ put: :edit
132
+ }
133
+
134
+ def initialize(controller, resources, options = {})
135
+ @controller = controller
136
+ @request = @controller.request
137
+ @format = @controller.formats.first
138
+ @resource = resources.last
139
+ @resources = resources
140
+ @options = options
141
+ @action = options.delete(:action)
142
+ @default_response = options.delete(:default_response)
143
+
144
+ if options[:location].respond_to?(:call)
145
+ location = options.delete(:location)
146
+ options[:location] = location.call unless has_errors?
147
+ end
148
+ end
149
+
150
+ delegate :head, :render, :redirect_to, to: :controller
151
+ delegate :get?, :post?, :patch?, :put?, :delete?, to: :request
152
+
153
+ # Undefine :to_json and :to_yaml since it's defined on Object
154
+ undef_method(:to_json) if method_defined?(:to_json)
155
+ undef_method(:to_yaml) if method_defined?(:to_yaml)
156
+
157
+ # Initializes a new responder and invokes the proper format. If the format is
158
+ # not defined, call to_format.
159
+ #
160
+ def self.call(*args)
161
+ new(*args).respond
162
+ end
163
+
164
+ # Main entry point for responder responsible to dispatch to the proper format.
165
+ #
166
+ def respond
167
+ method = "to_#{format}"
168
+ respond_to?(method) ? send(method) : to_format
169
+ end
170
+
171
+ # HTML format does not render the resource, it always attempt to render a
172
+ # template.
173
+ #
174
+ def to_html
175
+ default_render
176
+ rescue ActionView::MissingTemplate => e
177
+ navigation_behavior(e)
178
+ end
179
+
180
+ # to_js simply tries to render a template. If no template is found, raises the error.
181
+ def to_js
182
+ default_render
183
+ end
184
+
185
+ # All other formats follow the procedure below. First we try to render a
186
+ # template, if the template is not available, we verify if the resource
187
+ # responds to :to_format and display it.
188
+ #
189
+ def to_format
190
+ if !get? && has_errors? && !response_overridden?
191
+ display_errors
192
+ elsif has_view_rendering? || response_overridden?
193
+ default_render
194
+ else
195
+ api_behavior
196
+ end
197
+ rescue ActionView::MissingTemplate
198
+ api_behavior
199
+ end
200
+
201
+ protected
202
+
203
+ # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
204
+ def navigation_behavior(error)
205
+ if get?
206
+ raise error
207
+ elsif has_errors? && default_action
208
+ render error_rendering_options
209
+ else
210
+ redirect_to navigation_location, status: redirect_status
211
+ end
212
+ end
213
+
214
+ # This is the common behavior for formats associated with APIs, such as :xml and :json.
215
+ def api_behavior
216
+ raise MissingRenderer.new(format) unless has_renderer?
217
+
218
+ if get?
219
+ display resource
220
+ elsif post?
221
+ display resource, status: :created, location: api_location
222
+ else
223
+ head :no_content
224
+ end
225
+ end
226
+
227
+ # Returns the resource location by retrieving it from the options or
228
+ # returning the resources array.
229
+ #
230
+ def resource_location
231
+ options[:location] || resources
232
+ end
233
+ alias :navigation_location :resource_location
234
+ alias :api_location :resource_location
235
+
236
+ # If a response block was given, use it, otherwise call render on
237
+ # controller.
238
+ #
239
+ def default_render
240
+ if @default_response
241
+ @default_response.call(options)
242
+ elsif !get? && has_errors?
243
+ controller.render({ status: error_status }.merge!(options))
244
+ else
245
+ controller.render(options)
246
+ end
247
+ end
248
+
249
+ # Display is just a shortcut to render a resource with the current format.
250
+ #
251
+ # display @user, status: :ok
252
+ #
253
+ # For XML requests it's equivalent to:
254
+ #
255
+ # render xml: @user, status: :ok
256
+ #
257
+ # Options sent by the user are also used:
258
+ #
259
+ # respond_with(@user, status: :created)
260
+ # display(@user, status: :ok)
261
+ #
262
+ # Results in:
263
+ #
264
+ # render xml: @user, status: :created
265
+ #
266
+ def display(resource, given_options = {})
267
+ controller.render given_options.merge!(options).merge!(format => resource)
268
+ end
269
+
270
+ def display_errors
271
+ # TODO: use `error_status` once we switch the default to be `unprocessable_entity`,
272
+ # otherwise we'd be changing this behavior here now.
273
+ controller.render format => resource_errors, :status => :unprocessable_entity
274
+ end
275
+
276
+ # Check whether the resource has errors.
277
+ #
278
+ def has_errors?
279
+ resource.respond_to?(:errors) && !resource.errors.empty?
280
+ end
281
+
282
+ # Check whether the necessary Renderer is available
283
+ def has_renderer?
284
+ Renderers::RENDERERS.include?(format)
285
+ end
286
+
287
+ def has_view_rendering?
288
+ controller.class.include? ActionView::Rendering
289
+ end
290
+
291
+ # By default, render the <code>:edit</code> action for HTML requests with errors, unless
292
+ # the verb was POST.
293
+ #
294
+ def default_action
295
+ @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
296
+ end
297
+
298
+ def resource_errors
299
+ respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors
300
+ end
301
+
302
+ def json_resource_errors
303
+ { errors: resource.errors }
304
+ end
305
+
306
+ def response_overridden?
307
+ @default_response.present?
308
+ end
309
+
310
+ def error_rendering_options
311
+ if options[:render]
312
+ options[:render]
313
+ else
314
+ { action: default_action, status: error_status }
315
+ end
316
+ end
317
+ end
318
+ end
@@ -0,0 +1 @@
1
+ require_relative "responders"
@@ -0,0 +1,23 @@
1
+ require "zeitwerk"
2
+
3
+ module Responders
4
+ class Autoloader
5
+ class Inflector < Zeitwerk::Inflector
6
+ def camelize(basename, _abspath)
7
+ map = { version: "VERSION" }
8
+ map[basename.to_sym] || super
9
+ end
10
+ end
11
+
12
+ class << self
13
+ def setup
14
+ loader = Zeitwerk::Loader.new
15
+ loader.inflector = Inflector.new
16
+ lib = File.dirname(__dir__)
17
+ loader.push_dir(lib)
18
+ loader.ignore("#{lib}/jets-responders.rb")
19
+ loader.setup
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responders
4
+ # This responder modifies your current responder to redirect
5
+ # to the collection page on POST/PUT/DELETE.
6
+ module CollectionResponder
7
+ protected
8
+
9
+ # Returns the collection location for redirecting after POST/PUT/DELETE.
10
+ # This method, converts the following resources array to the following:
11
+ #
12
+ # [:admin, @post] #=> [:admin, :posts]
13
+ # [@user, @post] #=> [@user, :posts]
14
+ #
15
+ # When these new arrays are given to redirect_to, it will generate the
16
+ # proper URL pointing to the index action.
17
+ #
18
+ # [:admin, @post] #=> admin_posts_url
19
+ # [@user, @post] #=> user_posts_url(@user.to_param)
20
+ #
21
+ def navigation_location
22
+ return options[:location] if options[:location]
23
+
24
+ klass = resources.last.class
25
+
26
+ if klass.respond_to?(:model_name)
27
+ resources[0...-1] << klass.model_name.route_key.to_sym
28
+ else
29
+ resources
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responders
4
+ module ControllerMethod
5
+ # Adds the given responders to the current controller's responder, allowing you to cherry-pick
6
+ # which responders you want per controller.
7
+ #
8
+ # class InvitationsController < ApplicationController
9
+ # responders :flash, :http_cache
10
+ # end
11
+ #
12
+ # Takes symbols and strings and translates them to VariableResponder (eg. :flash becomes FlashResponder).
13
+ # Also allows passing in the responders modules in directly, so you could do:
14
+ #
15
+ # responders FlashResponder, HttpCacheResponder
16
+ #
17
+ # Or a mix of both methods:
18
+ #
19
+ # responders :flash, MyCustomResponder
20
+ #
21
+ def responders(*responders)
22
+ self.responder = responders.inject(Class.new(responder)) do |klass, responder|
23
+ responder = \
24
+ case responder
25
+ when Module
26
+ responder
27
+ when String, Symbol
28
+ Responders.const_get("#{responder.to_s.camelize}Responder")
29
+ else
30
+ raise "responder has to be a string, a symbol or a module"
31
+ end
32
+
33
+ klass.send(:include, responder)
34
+ klass
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ ActiveSupport.on_load(:action_controller_base) do
41
+ Jets::Controller::Base.extend Responders::ControllerMethod
42
+ end
@@ -0,0 +1,22 @@
1
+ module Responders
2
+ require "responders/controller_method"
3
+
4
+ class Engine < ::Jets::Engine
5
+ config.responders = ActiveSupport::OrderedOptions.new
6
+ config.responders.flash_keys = [:notice, :alert]
7
+ config.responders.namespace_lookup = false
8
+ config.responders.error_status = :ok
9
+ config.responders.redirect_status = :found
10
+
11
+ # Add load paths straight to I18n, so engines and application can overwrite it.
12
+ require "active_support/i18n"
13
+ I18n.load_path << File.expand_path("../responders/locales/en.yml", __dir__)
14
+
15
+ initializer "responders.flash_responder" do |app|
16
+ Responders::FlashResponder.flash_keys = app.config.responders.flash_keys
17
+ Responders::FlashResponder.namespace_lookup = app.config.responders.namespace_lookup
18
+ Jets::Controller::Responder.error_status = app.config.responders.error_status
19
+ Jets::Controller::Responder.redirect_status = app.config.responders.redirect_status
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responders
4
+ # Responder to automatically set flash messages based on I18n API. It checks for
5
+ # message based on the current action, but also allows defaults to be set, using
6
+ # the following order:
7
+ #
8
+ # flash.controller_name.action_name.status
9
+ # flash.actions.action_name.status
10
+ #
11
+ # So, if you have a CarsController, create action, it will check for:
12
+ #
13
+ # flash.cars.create.status
14
+ # flash.actions.create.status
15
+ #
16
+ # The statuses by default are :notice (when the object can be created, updated
17
+ # or destroyed with success) and :alert (when the object cannot be created
18
+ # or updated).
19
+ #
20
+ # On I18n, the resource_name given is available as interpolation option,
21
+ # this means you can set:
22
+ #
23
+ # flash:
24
+ # actions:
25
+ # create:
26
+ # notice: "Hooray! %{resource_name} was successfully created!"
27
+ #
28
+ # But sometimes, flash messages are not that simple. Going back
29
+ # to cars example, you might want to say the brand of the car when it's
30
+ # updated. Well, that's easy also:
31
+ #
32
+ # flash:
33
+ # cars:
34
+ # update:
35
+ # notice: "Hooray! You just tuned your %{car_brand}!"
36
+ #
37
+ # Since :car_name is not available for interpolation by default, you have
38
+ # to overwrite `flash_interpolation_options` in your controller.
39
+ #
40
+ # def flash_interpolation_options
41
+ # { :car_brand => @car.brand }
42
+ # end
43
+ #
44
+ # Then you will finally have:
45
+ #
46
+ # 'Hooray! You just tuned your Aston Martin!'
47
+ #
48
+ # If your controller is namespaced, for example Admin::CarsController,
49
+ # the messages will be checked in the following order:
50
+ #
51
+ # flash.admin.cars.create.status
52
+ # flash.admin.actions.create.status
53
+ # flash.cars.create.status
54
+ # flash.actions.create.status
55
+ #
56
+ # You can also have flash messages with embedded HTML. Just create a scope that
57
+ # ends with <tt>_html</tt> as the scopes below:
58
+ #
59
+ # flash.actions.create.notice_html
60
+ # flash.cars.create.notice_html
61
+ #
62
+ # == Options
63
+ #
64
+ # FlashResponder also accepts some options through respond_with API.
65
+ #
66
+ # * :flash - When set to false, no flash message is set.
67
+ #
68
+ # respond_with(@user, :flash => true)
69
+ #
70
+ # * :notice - Supply the message to be set if the record has no errors.
71
+ # * :alert - Supply the message to be set if the record has errors.
72
+ #
73
+ # respond_with(@user, :notice => "Hooray! Welcome!", :alert => "Woot! You failed.")
74
+ #
75
+ # * :flash_now - Sets the flash message using flash.now. Accepts true, :on_failure or :on_success.
76
+ #
77
+ # == Configure status keys
78
+ #
79
+ # As said previously, FlashResponder by default use :notice and :alert
80
+ # keys. You can change that by setting the status_keys:
81
+ #
82
+ # Responders::FlashResponder.flash_keys = [ :success, :failure ]
83
+ #
84
+ # However, the options :notice and :alert to respond_with are kept :notice
85
+ # and :alert.
86
+ #
87
+ module FlashResponder
88
+ class << self
89
+ attr_accessor :flash_keys, :namespace_lookup
90
+ end
91
+
92
+ self.flash_keys = [ :notice, :alert ]
93
+ self.namespace_lookup = false
94
+
95
+ def initialize(controller, resources, options = {})
96
+ super
97
+ @flash = options.delete(:flash)
98
+ @notice = options.delete(:notice)
99
+ @alert = options.delete(:alert)
100
+ @flash_now = options.delete(:flash_now) { :on_failure }
101
+ end
102
+
103
+ def to_html
104
+ set_flash_message! if set_flash_message?
105
+ super
106
+ end
107
+
108
+ def to_js
109
+ set_flash_message! if set_flash_message?
110
+ defined?(super) ? super : to_format
111
+ end
112
+
113
+ protected
114
+
115
+ def set_flash_message!
116
+ if has_errors?
117
+ status = Responders::FlashResponder.flash_keys.last
118
+ set_flash(status, @alert)
119
+ else
120
+ status = Responders::FlashResponder.flash_keys.first
121
+ set_flash(status, @notice)
122
+ end
123
+
124
+ return if controller.flash[status].present?
125
+
126
+ options = mount_i18n_options(status)
127
+ message = controller.helpers.t options[:default].shift, **options
128
+ set_flash(status, message)
129
+ end
130
+
131
+ def set_flash(key, value)
132
+ return if value.blank?
133
+ flash = controller.flash
134
+ flash = flash.now if set_flash_now?
135
+ flash[key] ||= value
136
+ end
137
+
138
+ def set_flash_now?
139
+ @flash_now == true || format == :js ||
140
+ (default_action && (has_errors? ? @flash_now == :on_failure : @flash_now == :on_success))
141
+ end
142
+
143
+ def set_flash_message? # :nodoc:
144
+ !get? && @flash != false
145
+ end
146
+
147
+ def mount_i18n_options(status) # :nodoc:
148
+ options = {
149
+ default: flash_defaults_by_namespace(status),
150
+ resource_name: resource_name,
151
+ downcase_resource_name: resource_name.downcase
152
+ }
153
+
154
+ controller_options = controller_interpolation_options
155
+ if controller_options
156
+ options.merge!(controller_options)
157
+ end
158
+
159
+ options
160
+ end
161
+
162
+ def controller_interpolation_options
163
+ controller.send(:flash_interpolation_options) if controller.respond_to?(:flash_interpolation_options, true)
164
+ end
165
+
166
+ def resource_name
167
+ if resource.class.respond_to?(:model_name)
168
+ resource.class.model_name.human
169
+ else
170
+ resource.class.name.underscore.humanize
171
+ end
172
+ end
173
+
174
+ def flash_defaults_by_namespace(status) # :nodoc:
175
+ defaults = []
176
+ slices = controller.controller_path.split("/")
177
+ lookup = Responders::FlashResponder.namespace_lookup
178
+
179
+ begin
180
+ controller_scope = :"flash.#{slices.fill(controller.controller_name, -1).join(".")}.#{controller.action_name}.#{status}"
181
+
182
+ actions_scope = lookup ? slices.fill("actions", -1).join(".") : :actions
183
+ actions_scope = :"flash.#{actions_scope}.#{controller.action_name}.#{status}"
184
+
185
+ defaults << :"#{controller_scope}_html"
186
+ defaults << controller_scope
187
+
188
+ defaults << :"#{actions_scope}_html"
189
+ defaults << actions_scope
190
+
191
+ slices.shift
192
+ end while slices.size > 0 && lookup
193
+
194
+ defaults << ""
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responders
4
+ # Set HTTP Last-Modified headers based on the given resource. It's used only
5
+ # on API behavior (to_format) and is useful for a client to check in the server
6
+ # if a resource changed after a specific date or not.
7
+ #
8
+ # This is not usually not used in html requests because pages contains a lot
9
+ # information besides the resource information, as current_user, flash messages,
10
+ # widgets... that are better handled with other strategies, as fragment caches and
11
+ # the digest of the body.
12
+ #
13
+ module HttpCacheResponder
14
+ def initialize(controller, resources, options = {})
15
+ super
16
+ @http_cache = options.delete(:http_cache)
17
+ end
18
+
19
+ def to_format
20
+ return if do_http_cache? && do_http_cache!
21
+ super
22
+ end
23
+
24
+ protected
25
+
26
+ def do_http_cache!
27
+ timestamp = resources.map do |resource|
28
+ resource.updated_at.try(:utc) if resource.respond_to?(:updated_at)
29
+ end.compact.max
30
+
31
+ controller.response.last_modified ||= timestamp if timestamp
32
+
33
+ head :not_modified if fresh = request.fresh?(controller.response)
34
+ fresh
35
+ end
36
+
37
+ def do_http_cache?
38
+ get? && @http_cache != false && Jets::Controller::Base.perform_caching &&
39
+ persisted? && resource.respond_to?(:updated_at)
40
+ end
41
+
42
+ def persisted?
43
+ resource.respond_to?(:persisted?) ? resource.persisted? : true
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ en:
2
+ flash:
3
+ actions:
4
+ create:
5
+ notice: '%{resource_name} was successfully created.'
6
+ # alert: '%{resource_name} could not be created.'
7
+ update:
8
+ notice: '%{resource_name} was successfully updated.'
9
+ # alert: '%{resource_name} could not be updated.'
10
+ destroy:
11
+ notice: '%{resource_name} was successfully destroyed.'
12
+ alert: '%{resource_name} could not be destroyed.'
@@ -0,0 +1,3 @@
1
+ module Responders
2
+ VERSION = "0.1.0"
3
+ end
data/lib/responders.rb ADDED
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.expand_path("../", __FILE__))
2
+
3
+ require "responders/autoloader"
4
+ Responders::Autoloader.setup
5
+
6
+ require "memoist"
7
+ require "rainbow/ext/string"
8
+
9
+ module Responders
10
+ class Error < StandardError; end
11
+ end
12
+
13
+ ActiveSupport.on_load(:jets_controller) do
14
+ include Jets::Controller::RespondWith
15
+ end
16
+
17
+ require "responders/engine"
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jets-responders
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tung Nguyen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-12-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: memoist
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rainbow
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
126
+ email:
127
+ - tung@boltops.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - CHANGELOG.md
134
+ - Gemfile
135
+ - Guardfile
136
+ - LICENSE.txt
137
+ - README.md
138
+ - Rakefile
139
+ - jets-responders.gemspec
140
+ - lib/jets-responders.rb
141
+ - lib/jets/controller/respond_with.rb
142
+ - lib/jets/controller/responder.rb
143
+ - lib/responders.rb
144
+ - lib/responders/autoloader.rb
145
+ - lib/responders/collection_responder.rb
146
+ - lib/responders/controller_method.rb
147
+ - lib/responders/engine.rb
148
+ - lib/responders/flash_responder.rb
149
+ - lib/responders/http_cache_responder.rb
150
+ - lib/responders/locales/en.yml
151
+ - lib/responders/version.rb
152
+ homepage: https://github.com/rubyonjets/jets-responders
153
+ licenses:
154
+ - MIT
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubygems_version: 3.4.20
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: A set of Jets responders to dry up your application
175
+ test_files: []