rest_framework 0.7.4 → 0.7.5
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 +4 -4
- data/VERSION +1 -1
- data/app/views/rest_framework/_head.html.erb +19 -10
- data/lib/rest_framework/controller_mixins/base.rb +14 -3
- data/lib/rest_framework/controller_mixins/bulk.rb +51 -0
- data/lib/rest_framework/controller_mixins/models.rb +28 -24
- data/lib/rest_framework/controller_mixins.rb +1 -0
- data/lib/rest_framework/routers.rb +9 -0
- data/lib/rest_framework/serializers.rb +5 -6
- data/lib/rest_framework.rb +4 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af670b7b95c56b3afafeb8947b732be7e5ff3adc9f26b7bdd8f3dc6705c48094
|
4
|
+
data.tar.gz: ba4de3b422fcae2451bbb5ea5e979acaaaedfba53773ddf3a77f16c97fd4db85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f98df76846047b3435c6caaf071ebd4ac60a9ddd9da517b621d21d9e84498a28d5387e2338128c4b7815d36f0750e6c06d84147409a9ce3982a329d626a9c60
|
7
|
+
data.tar.gz: 3aa62deb130160763d08b1487496973bdc587968c278cf26be0390d9e3464d4c408ea9f30d01d8fb38702a93e311df26a81b54f3b5adf9a49b344667af5d7508
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.7.
|
1
|
+
0.7.5
|
@@ -3,8 +3,20 @@
|
|
3
3
|
<%= csrf_meta_tags %>
|
4
4
|
<%= csp_meta_tag rescue nil %>
|
5
5
|
|
6
|
+
<!-- Bootstrap -->
|
6
7
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
|
7
|
-
<
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
|
9
|
+
|
10
|
+
<!-- Highlight.js -->
|
11
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/vs.min.css" integrity="sha512-AVoZ71dJLtHRlsgWwujPT1hk2zxtFWsPlpTPCc/1g0WgpbmlzkqlDFduAvnOV4JJWKUquPc1ZyMc5eq4fRnKOQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
12
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js" integrity="sha512-bgHRAiTjGrzHzLyKOnpFvaEpGzJet3z4tZnXGjpsCcqOnAH6VGUx9frc5bcIhKTVLEiCO6vEhNAgx5jtLUYrfA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
13
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/json.min.js" integrity="sha512-0xYvyncS9OLE7GOpNBZFnwyh9+bq4HVgk4yVVYI678xRvE22ASicF1v6fZ1UiST+M6pn17MzFZdvVCI3jTHSyw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
14
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/xml.min.js" integrity="sha512-5zBcw+OKRkaNyvUEPlTSfYylVzgpi7KpncY36b0gRudfxIYIH0q0kl2j26uCUB3YBRM6ytQQEZSgRg+ZlBTmdA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
15
|
+
|
16
|
+
<!-- NeatJSON -->
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
|
18
|
+
|
19
|
+
<!-- Custom Style -->
|
8
20
|
<style>
|
9
21
|
/* Adjust headers to always take up their entire row, and tweak the sizing. */
|
10
22
|
h1,h2,h3,h4,h5,h6 { display: inline-block; font-weight: normal; margin-bottom: 0; }
|
@@ -17,7 +29,7 @@ h6 { font-size: 1rem; }
|
|
17
29
|
|
18
30
|
/* Make code and code blocks a little nicer looking. */
|
19
31
|
code {
|
20
|
-
padding:
|
32
|
+
padding: .5em !important;
|
21
33
|
background-color: #f3f3f3 !important;
|
22
34
|
border: 1px solid #aaa;
|
23
35
|
border-radius: 3px;
|
@@ -56,25 +68,22 @@ code {
|
|
56
68
|
}
|
57
69
|
</style>
|
58
70
|
|
59
|
-
|
60
|
-
<script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
|
61
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/highlight.min.js" integrity="sha512-TDKKr+IvoqZnPzc3l35hdjpHD0m+b2EC2SrLEgKDRWpxf2rFCxemkgvJ5kfU48ip+Y+m2XVKyOCD85ybtlZDmw==" crossorigin="anonymous"></script>
|
62
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/json.min.js" integrity="sha512-FoN8JE+WWCdIGXAIT8KQXwpiavz0Mvjtfk7Rku3MDUNO0BDCiRMXAsSX+e+COFyZTcDb9HDgP+pM2RX12d4j+A==" crossorigin="anonymous"></script>
|
63
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/xml.min.js" integrity="sha512-dICltIgnUP+QSJrnYGCV8943p3qSDgvcg2NU4W8IcOZP4tdrvxlXjbhIznhtVQEcXow0mOjLM0Q6/NvZsmUH4g==" crossorigin="anonymous"></script>
|
71
|
+
<!-- Custom JavaScript -->
|
64
72
|
<script>
|
65
|
-
hljs.initHighlightingOnLoad()
|
66
|
-
|
67
73
|
// What to do when document loads.
|
68
74
|
document.addEventListener("DOMContentLoaded", (event) => {
|
69
75
|
// Pretty-print JSON.
|
70
76
|
[...document.getElementsByClassName("language-json")].forEach((element, index) => {
|
71
|
-
element.innerHTML = neatJSON(JSON.parse(element.
|
77
|
+
element.innerHTML = neatJSON(JSON.parse(element.innerText), {
|
72
78
|
wrap: 80,
|
73
79
|
afterComma: 1,
|
74
80
|
afterColon: 1,
|
75
81
|
})
|
76
82
|
});
|
77
83
|
|
84
|
+
// Then highlight it.
|
85
|
+
hljs.highlightAll();
|
86
|
+
|
78
87
|
// Insert copy link and callback to copy contents of `<code>` element.
|
79
88
|
[...document.getElementsByClassName("rrf-copy")].forEach((element, index) => {
|
80
89
|
element.insertAdjacentHTML(
|
@@ -18,7 +18,7 @@ module RESTFramework::BaseControllerMixin
|
|
18
18
|
filter_backends: nil,
|
19
19
|
singleton_controller: nil,
|
20
20
|
|
21
|
-
#
|
21
|
+
# Options related to metadata and display.
|
22
22
|
title: nil,
|
23
23
|
description: nil,
|
24
24
|
inflect_acronyms: ["ID", "REST", "API"].freeze,
|
@@ -36,6 +36,10 @@ module RESTFramework::BaseControllerMixin
|
|
36
36
|
page_size_query_param: "page_size",
|
37
37
|
max_page_size: nil,
|
38
38
|
|
39
|
+
# Options related to bulk actions and batch processing.
|
40
|
+
bulk_guard_query_param: nil,
|
41
|
+
enable_batch_processing: nil,
|
42
|
+
|
39
43
|
# Option to disable serializer adapters by default, mainly introduced because Active Model
|
40
44
|
# Serializers will do things like serialize `[]` into `{"":[]}`.
|
41
45
|
disable_adapters_by_default: true,
|
@@ -74,6 +78,13 @@ module RESTFramework::BaseControllerMixin
|
|
74
78
|
end
|
75
79
|
end
|
76
80
|
|
81
|
+
# Add builtin bulk actions.
|
82
|
+
RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
|
83
|
+
if self.method_defined?(action)
|
84
|
+
actions[action] = {path: "", methods: methods, type: :builtin}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
77
88
|
# Add extra actions.
|
78
89
|
if extra_actions = self.try(:extra_actions)
|
79
90
|
actions.merge!(RESTFramework::Utils.parse_extra_actions(extra_actions, controller: self))
|
@@ -268,8 +279,8 @@ module RESTFramework::BaseControllerMixin
|
|
268
279
|
)
|
269
280
|
end
|
270
281
|
|
271
|
-
#
|
272
|
-
#
|
282
|
+
# Render a browsable API for `html` format, along with basic `json`/`xml` formats, and with
|
283
|
+
# support or passing custom `kwargs` to the underlying `render` calls.
|
273
284
|
def api_response(payload, html_kwargs: nil, **kwargs)
|
274
285
|
html_kwargs ||= {}
|
275
286
|
json_kwargs = kwargs.delete(:json_kwargs) || {}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative "models"
|
2
|
+
|
3
|
+
# Mixin for creating records in bulk. This is unique compared to update/destroy because we overload
|
4
|
+
# the existing `create` action to support bulk creation.
|
5
|
+
module RESTFramework::BulkCreateModelMixin
|
6
|
+
def create
|
7
|
+
raise NotImplementedError, "TODO"
|
8
|
+
end
|
9
|
+
|
10
|
+
# Perform the `create!` call and return the created record.
|
11
|
+
def create_all!
|
12
|
+
raise NotImplementedError, "TODO"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Mixin for updating records in bulk.
|
17
|
+
module RESTFramework::BulkUpdateModelMixin
|
18
|
+
def update_all
|
19
|
+
raise NotImplementedError, "TODO"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Perform the `update!` call and return the updated record.
|
23
|
+
def update_all!
|
24
|
+
raise NotImplementedError, "TODO"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Mixin for destroying records in bulk.
|
29
|
+
module RESTFramework::BulkDestroyModelMixin
|
30
|
+
def destroy_all
|
31
|
+
raise NotImplementedError, "TODO"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
35
|
+
def destroy_all!
|
36
|
+
raise NotImplementedError, "TODO"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Mixin that includes all the CRUD bulk mixins.
|
41
|
+
module RESTFramework::BulkModelControllerMixin
|
42
|
+
include RESTFramework::ModelControllerMixin
|
43
|
+
|
44
|
+
include RESTFramework::BulkCreateModelMixin
|
45
|
+
include RESTFramework::BulkUpdateModelMixin
|
46
|
+
include RESTFramework::BulkDestroyModelMixin
|
47
|
+
|
48
|
+
def self.included(base)
|
49
|
+
RESTFramework::ModelControllerMixin.included(base)
|
50
|
+
end
|
51
|
+
end
|
@@ -163,15 +163,20 @@ module RESTFramework::BaseModelControllerMixin
|
|
163
163
|
# Get metadata about the resource's associations (reflections).
|
164
164
|
def get_associations_metadata
|
165
165
|
return self.get_model.reflections.map { |k, v|
|
166
|
+
begin
|
167
|
+
pk = v.active_record_primary_key
|
168
|
+
rescue ActiveRecord::UnknownPrimaryKey
|
169
|
+
end
|
170
|
+
|
166
171
|
next [k, {
|
167
172
|
macro: v.macro,
|
168
173
|
label: self.get_label(k),
|
169
174
|
class_name: v.class_name,
|
170
175
|
foreign_key: v.foreign_key,
|
171
|
-
primary_key:
|
176
|
+
primary_key: pk,
|
172
177
|
polymorphic: v.polymorphic?,
|
173
178
|
table_name: v.table_name,
|
174
|
-
options: v.options,
|
179
|
+
options: v.options.presence,
|
175
180
|
}.compact]
|
176
181
|
}.to_h
|
177
182
|
end
|
@@ -238,9 +243,10 @@ module RESTFramework::BaseModelControllerMixin
|
|
238
243
|
end
|
239
244
|
|
240
245
|
def self.included(base)
|
246
|
+
RESTFramework::BaseControllerMixin.included(base)
|
247
|
+
|
241
248
|
return unless base.is_a?(Class)
|
242
249
|
|
243
|
-
RESTFramework::BaseControllerMixin.included(base)
|
244
250
|
base.extend(ClassMethods)
|
245
251
|
|
246
252
|
# Add class attributes (with defaults) unless they already exist.
|
@@ -311,12 +317,12 @@ module RESTFramework::BaseModelControllerMixin
|
|
311
317
|
) || self.get_fields
|
312
318
|
end
|
313
319
|
|
314
|
-
#
|
320
|
+
# Get the configured serializer class, or `NativeSerializer` as a default.
|
315
321
|
def get_serializer_class
|
316
322
|
return super || RESTFramework::NativeSerializer
|
317
323
|
end
|
318
324
|
|
319
|
-
#
|
325
|
+
# Get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
|
320
326
|
def get_filter_backends
|
321
327
|
return self.class.filter_backends || [
|
322
328
|
RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
|
@@ -324,14 +330,16 @@ module RESTFramework::BaseModelControllerMixin
|
|
324
330
|
end
|
325
331
|
|
326
332
|
# Filter the request body for keys in current action's allowed_parameters/fields config.
|
327
|
-
def get_body_params
|
333
|
+
def get_body_params(request_parameters: nil)
|
334
|
+
request_parameters ||= request.request_parameters
|
335
|
+
|
328
336
|
# Filter the request body and map to strings. Return all params if we cannot resolve a list of
|
329
337
|
# allowed parameters or fields.
|
330
338
|
allowed_params = self.get_allowed_parameters&.map(&:to_s)
|
331
339
|
body_params = if allowed_params
|
332
|
-
|
340
|
+
request_parameters.select { |p| allowed_params.include?(p) }
|
333
341
|
else
|
334
|
-
|
342
|
+
request_parameters
|
335
343
|
end
|
336
344
|
|
337
345
|
# Add query params in place of missing body params, if configured.
|
@@ -370,7 +378,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
370
378
|
return @recordset = nil
|
371
379
|
end
|
372
380
|
|
373
|
-
#
|
381
|
+
# Get the records this controller has access to *after* any filtering is applied.
|
374
382
|
def get_records
|
375
383
|
return @records if instance_variable_defined?(:@records)
|
376
384
|
|
@@ -413,7 +421,7 @@ module RESTFramework::ListModelMixin
|
|
413
421
|
return api_response(self.get_index_records)
|
414
422
|
end
|
415
423
|
|
416
|
-
#
|
424
|
+
# Get records with both filtering and pagination applied.
|
417
425
|
def get_index_records
|
418
426
|
records = self.get_records
|
419
427
|
|
@@ -448,7 +456,7 @@ module RESTFramework::CreateModelMixin
|
|
448
456
|
return api_response(self.create!, status: :created)
|
449
457
|
end
|
450
458
|
|
451
|
-
#
|
459
|
+
# Perform the `create!` call and return the created record.
|
452
460
|
def create!
|
453
461
|
if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
|
454
462
|
# Create with any properties inherited from the recordset. We exclude any `select` clauses in
|
@@ -468,7 +476,7 @@ module RESTFramework::UpdateModelMixin
|
|
468
476
|
return api_response(self.update!)
|
469
477
|
end
|
470
478
|
|
471
|
-
#
|
479
|
+
# Perform the `update!` call and return the updated record.
|
472
480
|
def update!
|
473
481
|
record = self.get_record
|
474
482
|
record.update!(self.get_update_params)
|
@@ -483,7 +491,7 @@ module RESTFramework::DestroyModelMixin
|
|
483
491
|
return api_response("")
|
484
492
|
end
|
485
493
|
|
486
|
-
#
|
494
|
+
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
487
495
|
def destroy!
|
488
496
|
return self.get_record.destroy!
|
489
497
|
end
|
@@ -493,29 +501,25 @@ end
|
|
493
501
|
module RESTFramework::ReadOnlyModelControllerMixin
|
494
502
|
include RESTFramework::BaseModelControllerMixin
|
495
503
|
|
496
|
-
|
497
|
-
|
504
|
+
include RESTFramework::ListModelMixin
|
505
|
+
include RESTFramework::ShowModelMixin
|
498
506
|
|
507
|
+
def self.included(base)
|
499
508
|
RESTFramework::BaseModelControllerMixin.included(base)
|
500
509
|
end
|
501
|
-
|
502
|
-
include RESTFramework::ListModelMixin
|
503
|
-
include RESTFramework::ShowModelMixin
|
504
510
|
end
|
505
511
|
|
506
512
|
# Mixin that includes all the CRUD mixins.
|
507
513
|
module RESTFramework::ModelControllerMixin
|
508
514
|
include RESTFramework::BaseModelControllerMixin
|
509
515
|
|
510
|
-
def self.included(base)
|
511
|
-
return unless base.is_a?(Class)
|
512
|
-
|
513
|
-
RESTFramework::BaseModelControllerMixin.included(base)
|
514
|
-
end
|
515
|
-
|
516
516
|
include RESTFramework::ListModelMixin
|
517
517
|
include RESTFramework::ShowModelMixin
|
518
518
|
include RESTFramework::CreateModelMixin
|
519
519
|
include RESTFramework::UpdateModelMixin
|
520
520
|
include RESTFramework::DestroyModelMixin
|
521
|
+
|
522
|
+
def self.included(base)
|
523
|
+
RESTFramework::BaseModelControllerMixin.included(base)
|
524
|
+
end
|
521
525
|
end
|
@@ -102,6 +102,15 @@ module ActionDispatch::Routing
|
|
102
102
|
public_send(m, "", action: action) if self.respond_to?(m)
|
103
103
|
end
|
104
104
|
end
|
105
|
+
|
106
|
+
# Route bulk actions, if configured.
|
107
|
+
RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
|
108
|
+
next unless controller_class.method_defined?(action)
|
109
|
+
|
110
|
+
[methods].flatten.each do |m|
|
111
|
+
public_send(m, "", action: action) if self.respond_to?(m)
|
112
|
+
end
|
113
|
+
end
|
105
114
|
end
|
106
115
|
|
107
116
|
if unscoped
|
@@ -88,7 +88,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
88
88
|
return self.config || self.singular_config || self.plural_config
|
89
89
|
end
|
90
90
|
|
91
|
-
#
|
91
|
+
# Get a native serializer configuration from the controller.
|
92
92
|
def get_controller_native_serializer_config
|
93
93
|
return nil unless @controller
|
94
94
|
|
@@ -101,9 +101,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
101
101
|
return controller_serializer || @controller.class.try(:native_serializer_config)
|
102
102
|
end
|
103
103
|
|
104
|
-
#
|
105
|
-
#
|
106
|
-
# behavior:
|
104
|
+
# Filter a single subconfig for specific keys. By default, keys from `fields` are removed from the
|
105
|
+
# provided `subcfg`. There are two (mutually exclusive) options to adjust the behavior:
|
107
106
|
#
|
108
107
|
# `add`: Add any `fields` to the `subcfg` which aren't already in the `subcfg`.
|
109
108
|
# `only`: Remove any values found in the `subcfg` not in `fields`.
|
@@ -149,7 +148,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
149
148
|
return subcfg
|
150
149
|
end
|
151
150
|
|
152
|
-
#
|
151
|
+
# Filter out configuration properties based on the :except query parameter.
|
153
152
|
def filter_except(cfg)
|
154
153
|
return cfg unless @controller
|
155
154
|
|
@@ -245,7 +244,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
245
244
|
return filter_except(self._get_raw_serializer_config)
|
246
245
|
end
|
247
246
|
|
248
|
-
#
|
247
|
+
# Serialize a single record and merge results of `serializer_methods`.
|
249
248
|
def _serialize(record, config, serializer_methods)
|
250
249
|
# Ensure serializer_methods is either falsy, or an array.
|
251
250
|
if serializer_methods && !serializer_methods.respond_to?(:to_ary)
|
data/lib/rest_framework.rb
CHANGED
@@ -13,6 +13,10 @@ module RESTFramework
|
|
13
13
|
RRF_BUILTIN_ACTIONS = {
|
14
14
|
options: :options,
|
15
15
|
}.freeze
|
16
|
+
RRF_BUILTIN_BULK_ACTIONS = {
|
17
|
+
update_all: [:put, :patch].freeze,
|
18
|
+
destroy_all: :delete,
|
19
|
+
}.freeze
|
16
20
|
|
17
21
|
# Global configuration should be kept minimal, as controller-level configurations allows multiple
|
18
22
|
# APIs to be defined to behave differently.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rest_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gregory N. Schmit
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-01-
|
11
|
+
date: 2023-01-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -42,6 +42,7 @@ files:
|
|
42
42
|
- lib/rest_framework.rb
|
43
43
|
- lib/rest_framework/controller_mixins.rb
|
44
44
|
- lib/rest_framework/controller_mixins/base.rb
|
45
|
+
- lib/rest_framework/controller_mixins/bulk.rb
|
45
46
|
- lib/rest_framework/controller_mixins/models.rb
|
46
47
|
- lib/rest_framework/engine.rb
|
47
48
|
- lib/rest_framework/errors.rb
|