undercarriage 0.5.7 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c80a0b92962c21262508fe3cfe89796a348e1b87c9e950d56e403ff114ba9c20
4
- data.tar.gz: ec30e3665b643a807e05ca1f4ced57c385bceb5ae1a7dae6db16d72f429d0232
3
+ metadata.gz: b8d42d984572277bb1282c5fa0306028d91ced6801f88bb7ad36f01c05fcd8e8
4
+ data.tar.gz: f3d7690b1fdbf864395695f4a8651bbce2a907333430df01208edd0503419144
5
5
  SHA512:
6
- metadata.gz: a276544a8b9343c1224d02918846fde1b426aa193a46557f3d52b2b82280c7d6ab5941a1dfe59ecefc02959c1bf72078cd19588aa01ea916dfe047b7c1b5344b
7
- data.tar.gz: 134eec2dea285ff0a3de8521f9e0af1d2c20c4601ff1b2b07b7954a69ee00d1edb79b22939c635f7cdff9bdf4f610f248ce089142d7ccc81d04e075eaebc3407
6
+ metadata.gz: 6ee506e32efb9f323e4f5dcfd8494d00050dc0e20a9d4c09244440c0b7269cc52e5024878a91e24db6da67aa88908840931c0559d0ab7028f1a4cb7e932e61cc
7
+ data.tar.gz: 8577454663eaec4d8b20bd5f06bf36bc2d296cca855113e77b5f9b91c33908354f69aa33645e82ba14c3f45e154790008f4d2c309bf4943814ab5b2d8fd28078
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2021 David Freerksen
1
+ Copyright 2026 David Freerksen
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,71 +1,94 @@
1
1
  # Undercarriage
2
2
 
3
- **\*Undercarriage is currently under development. It is not ready for production use.\***
4
-
5
- [![CircleCI](https://circleci.com/gh/dfreerksen/undercarriage.svg?style=shield)](https://circleci.com/gh/dfreerksen/undercarriage)
6
- [![Maintainability](https://api.codeclimate.com/v1/badges/d15980a534f8db936469/maintainability)](https://codeclimate.com/github/dfreerksen/undercarriage/maintainability)
7
- [![Test Coverage](https://api.codeclimate.com/v1/badges/d15980a534f8db936469/test_coverage)](https://codeclimate.com/github/dfreerksen/undercarriage/test_coverage)
8
-
9
3
  Undercarriage is a set of concerns to add to your application to trim some of the fat from controllers and models.
10
4
 
11
5
  ## Requirements
12
6
 
13
- * Ruby >= 2.6
7
+ * Ruby >= 3.0
14
8
  * Rails >= 6.0
15
9
 
16
10
  ## Installation
17
11
 
18
12
  Add to your application's Gemfile
19
13
 
20
- ```
21
- gem 'undercarriage', '~> 0.5'
14
+ ```bash
15
+ gem 'undercarriage', '~> 1.1'
22
16
  ```
23
17
 
24
18
  Run the bundle command
25
19
 
26
- ```
20
+ ```bash
27
21
  $ bundle install
28
22
  ```
29
23
 
30
24
  ## Usage
31
25
 
32
- TODO
26
+ Include `Undercarriage::Controllers::RestfulConcern` in a controller to get full RESTful `index`/`show`/`new`/`create`/`edit`/`update`/`destroy` actions, driven off the controller's own name/path:
27
+
28
+ ```ruby
29
+ class PostsController < ApplicationController
30
+ include Undercarriage::Controllers::RestfulConcern
31
+
32
+ private
33
33
 
34
- ## TODO
34
+ def permitted_attributes
35
+ [:title, :body]
36
+ end
37
+ end
38
+ ```
39
+
40
+ This infers `Post` as the model, sets `@posts`/`@post` as appropriate, and wires up flash messages, strong params, and redirects with no further code. Override the `*_content` hooks (e.g. `show_content`, `create_content`) or `after_create_action`/`after_update_action` to customize a single action without redefining it — see the YARD docs on each `Undercarriage::Controllers::Restful::*` concern for the full hook list.
41
+
42
+ The standalone concerns can be included individually where you don't want the full RESTful stack:
43
+
44
+ ```ruby
45
+ class ExamplesController < ApplicationController
46
+ include Undercarriage::Controllers::ActionConcern # action?/index_action?/etc. view helpers
47
+ include Undercarriage::Controllers::KaminariConcern # page_num/per_page params for Kaminari
48
+ include Undercarriage::Controllers::LocaleConcern # I18n.locale from HTTP_ACCEPT_LANGUAGE
49
+ end
50
+
51
+ class Example < ApplicationRecord
52
+ include Undercarriage::Models::PublishedConcern # published/unpublished scopes
53
+ end
54
+ ```
35
55
 
36
- * [ ] Allow a way to set locale instead of relying on browser preferred language in `Undercarriage::Controllers::LocaleConcern`
37
- * [ ] Add support for Rails 5.2
56
+ See the YARD documentation linked below for every concern's options and examples.
38
57
 
39
58
  ## Testing
40
59
 
41
60
  Run tests with one of the following
42
61
 
43
- ```
62
+ ```bash
44
63
  $ bundle exec rspec
45
64
  $ bundle exec rspec spec
46
65
  ```
47
66
 
48
67
  ### Appraisal
49
68
 
50
- Undercarriage uses [Appraisal](https://github.com/thoughtbot/appraisal) to ensure various dependency versions work as expected
69
+ Undercarriage uses [Appraisal2](https://github.com/appraisal-rb/appraisal2) (a maintained fork of [Appraisal](https://github.com/thoughtbot/appraisal), still exposing the `appraisal` executable) to ensure various dependency versions work as expected
51
70
 
52
71
  When dependencies change, run
53
72
 
54
- ```
73
+ ```bash
55
74
  $ bundle exec appraisal install
75
+ $ bundle exec appraisal generate-install
56
76
  ```
57
77
 
58
78
  To run tests with Appraisal, run
59
79
 
60
- ```
80
+ ```bash
61
81
  $ bundle exec appraisal rspec
62
82
  ```
63
83
 
64
- To run tests with a specific version of Appraisal, run
65
-
66
- ```
67
- $ bundle exec appraisal rails-60 rspec
68
- $ bundle exec appraisal rails-61 rspec
84
+ ```bash
85
+ $ bundle exec appraisal rails-6-0 rspec spec
86
+ $ bundle exec appraisal rails-6-1 rspec spec
87
+ $ bundle exec appraisal rails-7-0 rspec spec
88
+ $ bundle exec appraisal rails-7-1 rspec spec
89
+ $ bundle exec appraisal rails-7-2 rspec spec
90
+ $ bundle exec appraisal rails-8-0 rspec spec
91
+ $ bundle exec appraisal rails-8-1 rspec spec
69
92
  ```
70
93
 
71
94
  ## Code Analysis
@@ -76,24 +99,32 @@ Various tools are used to ensure code is linted and formatted correctly.
76
99
 
77
100
  [RuboCop](https://github.com/bbatsov/rubocop) is a Ruby static code analyzer.
78
101
 
79
- ```
102
+ ```bash
80
103
  $ rubocop
81
104
  ```
82
105
 
106
+ ### YARD-Lint
107
+
108
+ [YARD-Lint](https://github.com/mensfeld/yard-lint) is a linter for YARD documentation.
109
+
110
+ ```bash
111
+ $ bundle exec yard-lint
112
+ ```
113
+
83
114
  ## Documentation
84
115
 
85
116
  [Yard](https://github.com/lsegal/yard) is used to generate documentation. [Online documentation is available](http://www.rubydoc.info/github/dfreerksen/undercarriage/master)
86
117
 
87
118
  Build the documentation with one of the following
88
119
 
89
- ```
120
+ ```bash
90
121
  $ yard
91
122
  $ yard doc
92
123
  ```
93
124
 
94
125
  Build the documentation and list all undocumented objects
95
126
 
96
- ```
127
+ ```bash
97
128
  $ yard stats --list-undoc
98
129
  ```
99
130
 
data/Rakefile CHANGED
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  begin
4
- require 'bundler/setup'
4
+ require "bundler/setup"
5
5
  rescue LoadError
6
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
7
7
  end
8
8
 
9
- require 'rdoc/task'
9
+ require "rdoc/task"
10
10
 
11
11
  RDoc::Task.new(:rdoc) do |rdoc|
12
- rdoc.rdoc_dir = 'rdoc'
13
- rdoc.title = 'Undercarriage'
14
- rdoc.options << '--line-numbers'
15
- rdoc.rdoc_files.include('README.md')
16
- rdoc.rdoc_files.include('lib/**/*.rb')
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "Undercarriage"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
17
17
  end
18
18
 
19
- require 'bundler/gem_tasks'
19
+ require "bundler/gem_tasks"
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :nodoc:
3
4
  module Undercarriage
4
5
  # :nodoc:
5
6
  module Controllers
@@ -8,11 +9,10 @@ module Undercarriage
8
9
  #
9
10
  # Helpers for the controller or view to help identify the action
10
11
  #
11
- # Usage
12
+ # @example Controller
12
13
  # class ExamplesController < ApplicationController
13
14
  # include Undercarriage::Controllers::ActionConcern
14
15
  # end
15
- #
16
16
  module ActionConcern
17
17
  extend ActiveSupport::Concern
18
18
 
@@ -38,14 +38,13 @@ module Undercarriage
38
38
  #
39
39
  # Check if action is a certain action type
40
40
  #
41
- # Usage
42
- # action?(:show) # true
43
- # action?('show') # true
44
- # action?(:index) # false
45
- #
46
41
  # @param action_method [String, Symbol] the action to test
47
42
  # @return [Boolean] if action matches
48
43
  #
44
+ # @example View
45
+ # action?(:show) # true
46
+ # action?("show") # true
47
+ # action?(:index) # false
49
48
  def action?(action_method)
50
49
  action == action_method.to_sym
51
50
  end
@@ -55,14 +54,13 @@ module Undercarriage
55
54
  #
56
55
  # Check if action is the index action type. The check will pass if it is an `index` action
57
56
  #
58
- # Usage
59
- # index_action? # true
60
- # index_action? # false
61
- #
62
57
  # @return [Boolean] if action is action type
63
58
  #
59
+ # @example View
60
+ # index_action? # true
61
+ # index_action? # false
64
62
  def index_action?
65
- action?('index')
63
+ action?("index")
66
64
  end
67
65
 
68
66
  ##
@@ -70,14 +68,13 @@ module Undercarriage
70
68
  #
71
69
  # Check if action is the show action type. The check will pass if it is a `show` action
72
70
  #
73
- # Usage
74
- # show_action? # true
75
- # show_action? # false
76
- #
77
71
  # @return [Boolean] if action is action type
78
72
  #
73
+ # @example View
74
+ # show_action? # true
75
+ # show_action? # false
79
76
  def show_action?
80
- action?('show')
77
+ action?("show")
81
78
  end
82
79
 
83
80
  ##
@@ -85,14 +82,13 @@ module Undercarriage
85
82
  #
86
83
  # Check if action is the new action type. The check will pass if it is a `new` action
87
84
  #
88
- # Usage
89
- # new_action? # true
90
- # new_action? # false
91
- #
92
85
  # @return [Boolean] if action is action type
93
86
  #
87
+ # @example View
88
+ # new_action? # true
89
+ # new_action? # false
94
90
  def new_action?
95
- action?('new')
91
+ action?("new")
96
92
  end
97
93
 
98
94
  ##
@@ -100,14 +96,13 @@ module Undercarriage
100
96
  #
101
97
  # Check if action is the create action type. The check will pass if it is a `create` action
102
98
  #
103
- # Usage
104
- # create_action? # true
105
- # create_action? # false
106
- #
107
99
  # @return [Boolean] if action is action type
108
100
  #
101
+ # @example View
102
+ # create_action? # true
103
+ # create_action? # false
109
104
  def create_action?
110
- action?('create')
105
+ action?("create")
111
106
  end
112
107
 
113
108
  ##
@@ -115,14 +110,13 @@ module Undercarriage
115
110
  #
116
111
  # Check if action is the edit action type. The check will pass if it is an `edit` action
117
112
  #
118
- # Usage
119
- # edit_action? # true
120
- # edit_action? # false
121
- #
122
113
  # @return [Boolean] if action is action type
123
114
  #
115
+ # @example View
116
+ # edit_action? # true
117
+ # edit_action? # false
124
118
  def edit_action?
125
- action?('edit')
119
+ action?("edit")
126
120
  end
127
121
 
128
122
  ##
@@ -130,14 +124,13 @@ module Undercarriage
130
124
  #
131
125
  # Check if action is the update action type. The check will pass if it is an `update` action
132
126
  #
133
- # Usage
134
- # update_action? # true
135
- # update_action? # false
136
- #
137
127
  # @return [Boolean] if action is action type
138
128
  #
129
+ # @example View
130
+ # update_action? # true
131
+ # update_action? # false
139
132
  def update_action?
140
- action?('update')
133
+ action?("update")
141
134
  end
142
135
 
143
136
  ##
@@ -145,14 +138,13 @@ module Undercarriage
145
138
  #
146
139
  # Check if action is the destroy action type. The check will pass if it is a `destroy` action
147
140
  #
148
- # Usage
149
- # destroy_action? # true
150
- # destroy_action? # false
151
- #
152
141
  # @return [Boolean] if action is action type
153
142
  #
143
+ # @example View
144
+ # destroy_action? # true
145
+ # destroy_action? # false
154
146
  def destroy_action?
155
- action?('destroy')
147
+ action?("destroy")
156
148
  end
157
149
 
158
150
  ##
@@ -160,12 +152,11 @@ module Undercarriage
160
152
  #
161
153
  # Check if action is a collection action type. An action is a collection type if it is the `index` action
162
154
  #
163
- # Usage
164
- # collection_action? # true
165
- # collection_action? # false
166
- #
167
155
  # @return [Boolean] if action is collection type
168
156
  #
157
+ # @example View
158
+ # collection_action? # true
159
+ # collection_action? # false
169
160
  def collection_action?
170
161
  collection_actions.include?(action)
171
162
  end
@@ -175,15 +166,14 @@ module Undercarriage
175
166
  #
176
167
  # Check if action is a create or new action type. The check will pass if it is a `create` or `new` action
177
168
  #
178
- # Usage
169
+ # @return [Boolean] if action is actions type
170
+ #
171
+ # @example View create
179
172
  # create_actions? # true
180
173
  # create_actions? # false
181
- #
174
+ # @example View new
182
175
  # new_actions? # true
183
176
  # new_actions? # false
184
- #
185
- # @return [Boolean] if action is actions type
186
- #
187
177
  def create_actions?
188
178
  create_actions.include?(action)
189
179
  end
@@ -195,12 +185,11 @@ module Undercarriage
195
185
  # Check if action is a member action type. An action is a member type if it is the `edit`, `show`, or `update`
196
186
  # action
197
187
  #
198
- # Usage
199
- # member_action? # true
200
- # member_action? # false
201
- #
202
188
  # @return [Boolean] if action is member type
203
189
  #
190
+ # @example View
191
+ # member_action? # true
192
+ # member_action? # false
204
193
  def member_action?
205
194
  member_actions.include?(action)
206
195
  end
@@ -210,15 +199,14 @@ module Undercarriage
210
199
  #
211
200
  # Check if action is an edit or update action type. The check will pass if it is an `edit` or `update` action
212
201
  #
213
- # Usage
202
+ # @return [Boolean] if action is actions type
203
+ #
204
+ # @example View update
214
205
  # update_actions? # true
215
206
  # update_actions? # false
216
- #
207
+ # @example View edit
217
208
  # edit_actions? # true
218
209
  # edit_actions? # false
219
- #
220
- # @return [Boolean] if action is actions type
221
- #
222
210
  def update_actions?
223
211
  update_actions.include?(action)
224
212
  end
@@ -231,6 +219,7 @@ module Undercarriage
231
219
  #
232
220
  # Take `action_name` (string) and turn it into a symbol
233
221
  #
222
+ # @return [Symbol] action_name as a symbol
234
223
  def action
235
224
  action_name.to_sym
236
225
  end
@@ -238,6 +227,7 @@ module Undercarriage
238
227
  ##
239
228
  # Collection actions
240
229
  #
230
+ # @return [Array] collection actions
241
231
  def collection_actions
242
232
  %i[index]
243
233
  end
@@ -245,6 +235,7 @@ module Undercarriage
245
235
  ##
246
236
  # Member actions
247
237
  #
238
+ # @return [Array] member actions
248
239
  def member_actions
249
240
  %i[edit show update]
250
241
  end
@@ -252,6 +243,7 @@ module Undercarriage
252
243
  ##
253
244
  # Create actions
254
245
  #
246
+ # @return [Array] create actions
255
247
  def create_actions
256
248
  %i[create new]
257
249
  end
@@ -259,6 +251,7 @@ module Undercarriage
259
251
  ##
260
252
  # Update actions
261
253
  #
254
+ # @return [Array] update actions
262
255
  def update_actions
263
256
  %i[edit update]
264
257
  end
@@ -9,7 +9,7 @@ module Undercarriage
9
9
  # Helpers for Kaminari style pagination. Note that the Kaminari gem is not loaded with dependency. It must be added
10
10
  # to your own Gemfile
11
11
  #
12
- # Usage
12
+ # @example Controller
13
13
  # class ExamplesController < ApplicationController
14
14
  # include Undercarriage::Controllers::KaminariConcern
15
15
  #
@@ -17,7 +17,6 @@ module Undercarriage
17
17
  # @examples = Examples.page(page_num).per(per_page)
18
18
  # end
19
19
  # end
20
- #
21
20
  module KaminariConcern
22
21
  extend ActiveSupport::Concern
23
22
 
@@ -29,35 +28,36 @@ module Undercarriage
29
28
  # Items per page
30
29
  #
31
30
  # The number of items to return in pagination. Will use the Kaminari config `default_per_page` (typically `25`)
32
- # for the count and will look for `per` in the URL paramaters to override.
31
+ # for the count and will look for `per` in the URL paramaters to override. The result is clamped between `1`
32
+ # and {#per_page_max} so a caller cannot force an unbounded (or negative/zero) number of records per page.
33
33
  #
34
34
  # This is asseccible from the View as `per_page`
35
35
  #
36
- # Usage
37
- # /examples?per=100 # Return 100 items per page
38
- # /examples?per=10&page=3 # Return page 3 of items with 10 items per page
39
- #
40
36
  # @return [Integer] the number of items per page
41
37
  #
38
+ # @example Request
39
+ # # GET /examples?per=100 # Return 100 items per page
40
+ # # GET /examples?per=10&page=3 # Return page 3 of items with 10 items per page
41
+ # # GET /examples?per=999999999 # Clamped down to per_page_max
42
42
  def per_page
43
- params.fetch(per_page_key, per_page_default).to_i
43
+ params.fetch(per_page_key, per_page_default).to_i.clamp(1, per_page_max)
44
44
  end
45
45
 
46
46
  ##
47
47
  # Page number
48
48
  #
49
- # Will look for the Kaminari config `param_name` (typically `page`) in the URL paramaters.
49
+ # Will look for the Kaminari config `param_name` (typically `page`) in the URL paramaters. The result is
50
+ # clamped to a minimum of `1` so a caller cannot force a negative or zero page number.
50
51
  #
51
52
  # This is asseccible from the View as `page_num`
52
53
  #
53
- # Usage
54
- # /examples?page=5 # Return page 5 of items
55
- # /examples?per=10&page=3 Return page 3 of items with 10 items per page
56
- #
57
54
  # @return [Integer] the page number
58
55
  #
56
+ # @example Request
57
+ # # GET /examples?page=5 # Return page 5 of items
58
+ # # GET /examples?per=10&page=3 # Return page 3 of items with 10 items per page
59
59
  def page_num
60
- params.fetch(page_num_key, page_num_default).to_i
60
+ params.fetch(page_num_key, page_num_default).to_i.clamp(1, nil)
61
61
  end
62
62
 
63
63
  protected
@@ -67,6 +67,7 @@ module Undercarriage
67
67
  #
68
68
  # Query param to be used to identify count to be returned
69
69
  #
70
+ # @return [Integer] per page count
70
71
  def per_page_key
71
72
  :per
72
73
  end
@@ -76,19 +77,43 @@ module Undercarriage
76
77
  #
77
78
  # Query param to be used to identify page offset
78
79
  #
80
+ # @return [String,Symbol] page number key
79
81
  def page_num_key
80
82
  Kaminari.config.param_name
81
83
  end
82
84
 
83
85
  private
84
86
 
87
+ ##
88
+ # Items per page default
89
+ #
90
+ # Fallback used when the `per` query param is absent.
91
+ #
92
+ # @return [Integer] default per page count
85
93
  def per_page_default
86
94
  Kaminari.config.default_per_page
87
95
  end
88
96
 
97
+ ##
98
+ # Page number default
99
+ #
100
+ # Fallback used when the page query param is absent.
101
+ #
102
+ # @return [Integer] default page number
89
103
  def page_num_default
90
104
  1
91
105
  end
106
+
107
+ ##
108
+ # Items per page maximum
109
+ #
110
+ # Upper bound enforced on {#per_page} regardless of what the `per` query param requests. Defaults to the
111
+ # Kaminari config `max_per_page` when set, otherwise `100`. Override to allow a different ceiling.
112
+ #
113
+ # @return [Integer] maximum per page count
114
+ def per_page_max
115
+ Kaminari.config.max_per_page || 100
116
+ end
92
117
  end
93
118
  end
94
119
  end
@@ -13,11 +13,10 @@ module Undercarriage
13
13
  # When preferred language cannot be identified or no translation is available, fall back to `I18n.default_locale`
14
14
  # (typically `en`).
15
15
  #
16
- # Usage
16
+ # @example Controller
17
17
  # class ExamplesController < ApplicationController
18
18
  # include Undercarriage::Controllers::LocaleConcern
19
19
  # end
20
- #
21
20
  module LocaleConcern
22
21
  extend ActiveSupport::Concern
23
22
 
@@ -32,10 +31,11 @@ module Undercarriage
32
31
  #
33
32
  # Helper for Views to return the identified language.
34
33
  #
35
- # Usage
36
- # <html lang="<%= html_lang %>"> #=> '<html lang="de">'
37
- # <html lang="<%= html_lang %>"> #=> '<html lang="de-at">'
34
+ # @return [String] locale for html tag lang attribute
38
35
  #
36
+ # @example View
37
+ # # <html lang="<%= html_lang %>"> #=> '<html lang="de">'
38
+ # # <html lang="<%= html_lang %>"> #=> '<html lang="de-at">'
39
39
  def html_lang
40
40
  I18n.locale.to_s
41
41
  end
@@ -54,14 +54,15 @@ module Undercarriage
54
54
  # * Persian/Farsi
55
55
  # * Urdu
56
56
  #
57
- # Usage
58
- # <html dir="<%= html_dir %>"> #=> <html dir="ltr">
59
- # <html dir="<%= html_dir %>"> #=> <html dir="rtl">
57
+ # @return [String] direction for html tag dir attribute
60
58
  #
59
+ # @example View
60
+ # # <html dir="<%= html_dir %>"> #=> <html dir="ltr">
61
+ # # <html dir="<%= html_dir %>"> #=> <html dir="rtl">
61
62
  def html_dir
62
63
  rtl_languages = %w[am ar az dv fa he ur]
63
64
 
64
- html_lang.start_with?(*rtl_languages) ? 'rtl' : 'ltr'
65
+ html_lang.start_with?(*rtl_languages) ? "rtl" : "ltr"
65
66
  end
66
67
 
67
68
  protected
@@ -71,12 +72,20 @@ module Undercarriage
71
72
  #
72
73
  # Set I18n locale for the request
73
74
  #
75
+ # @return [String] locale
74
76
  def identify_locale(&action)
75
77
  I18n.with_locale(first_available_locale, &action)
76
78
  end
77
79
 
78
80
  private
79
81
 
82
+ ##
83
+ # First available locale
84
+ #
85
+ # Intersects the request's accepted languages (falling back to `I18n.default_locale`) against
86
+ # `I18n.available_locales`, preferring the request's order.
87
+ #
88
+ # @return [String] locale
80
89
  def first_available_locale
81
90
  preferred_locales = (accepted_languages_header << I18n.default_locale.to_s).uniq
82
91
  available_locales = I18n.available_locales.map(&:to_s)
@@ -84,11 +93,18 @@ module Undercarriage
84
93
  (preferred_locales & available_locales).first
85
94
  end
86
95
 
96
+ ##
97
+ # Accepted languages header
98
+ #
99
+ # Parses the `HTTP_ACCEPT_LANGUAGE` request header into an ordered list of language tags, stripping any
100
+ # `;q=` quality values.
101
+ #
102
+ # @return [Array<String>] accepted language tags
87
103
  def accepted_languages_header
88
- accepted_languages = request.env['HTTP_ACCEPT_LANGUAGE'] || ''
104
+ accepted_languages = request.env["HTTP_ACCEPT_LANGUAGE"] || ""
89
105
 
90
- accepted_languages.gsub(/\s+/, '').split(',').map do |lang|
91
- lang.split(';q=').first
106
+ accepted_languages.gsub(/\s+/, "").split(",").map do |lang|
107
+ lang.split(";q=").first
92
108
  end
93
109
  end
94
110
  end