auxiliary_rails 0.2.0 → 0.3.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 +4 -4
- data/.gitlab-ci.yml +26 -0
- data/.rubocop.yml +11 -2
- data/.rubocop_todo.yml +13 -9
- data/.yardopts +5 -0
- data/CHANGELOG.md +19 -2
- data/CONTRIBUTING.md +0 -6
- data/Gemfile.lock +20 -16
- data/README.md +204 -3
- data/auxiliary_rails.gemspec +7 -5
- data/bin/rubocop +3 -0
- data/lib/auxiliary_rails.rb +5 -3
- data/lib/auxiliary_rails/application/command.rb +56 -0
- data/lib/auxiliary_rails/application/error.rb +10 -0
- data/lib/auxiliary_rails/application/form.rb +30 -0
- data/lib/auxiliary_rails/application/query.rb +78 -0
- data/lib/auxiliary_rails/cli.rb +19 -5
- data/lib/auxiliary_rails/concerns/performable.rb +141 -0
- data/lib/auxiliary_rails/version.rb +1 -1
- data/lib/generators/auxiliary_rails/install_commands_generator.rb +5 -0
- data/lib/generators/auxiliary_rails/install_generator.rb +0 -1
- data/lib/generators/auxiliary_rails/templates/application_error_template.rb +1 -1
- data/lib/generators/auxiliary_rails/templates/commands/application_command_template.rb +1 -1
- data/lib/generators/auxiliary_rails/templates/commands/command_template.rb +2 -2
- data/lib/generators/auxiliary_rails/templates/commands/commands.en_template.yml +5 -0
- data/templates/rails/elementary.rb +45 -9
- metadata +43 -11
- data/lib/auxiliary_rails/abstract_command.rb +0 -95
- data/lib/auxiliary_rails/abstract_error.rb +0 -7
- data/lib/generators/auxiliary_rails/install_rubocop_generator.rb +0 -29
- data/lib/generators/auxiliary_rails/templates/rubocop/rubocop_auxiliary_rails_template.yml +0 -65
- data/lib/generators/auxiliary_rails/templates/rubocop/rubocop_template.yml +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2dca627a51fe1045de4e00f82cd2ac9dcc96df20816d7de67a8bf818180a06c
|
4
|
+
data.tar.gz: e5ae6c889d8ed752d382d86822410a409daaf9f14b04cbf833938af32adab6e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cabb77bb72b361996f1bd9623768d7c1af219c0f1bed3a516781493b0b77b7892bb42a3e4ffbefaabd879862fd7b3f25872a87e814e6b7aa7ec2bfc51e1e93d1
|
7
|
+
data.tar.gz: 07d26c7e442836147af787b33b5d6300f8e3d01c4b60a53fd31cbf0381158a7af5f16f8cb67af8d395ac8df25333cc75de8edaf8d1b0eb01e5cb1f708a1ebc43
|
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
image: ruby:2.6
|
2
|
+
|
3
|
+
cache:
|
4
|
+
paths:
|
5
|
+
- vendor/bundle
|
6
|
+
- vendor/ruby
|
7
|
+
|
8
|
+
before_script:
|
9
|
+
- ruby -v
|
10
|
+
- gem install bundler --no-document
|
11
|
+
- bundle config set path 'vendor'
|
12
|
+
- bundle install --jobs $(nproc)
|
13
|
+
|
14
|
+
rspec:
|
15
|
+
stage: test
|
16
|
+
script:
|
17
|
+
- bundle exec rspec
|
18
|
+
|
19
|
+
rubocop:
|
20
|
+
stage: test
|
21
|
+
allow_failure: true
|
22
|
+
script:
|
23
|
+
- bundle exec rubocop
|
24
|
+
|
25
|
+
stages:
|
26
|
+
- test
|
data/.rubocop.yml
CHANGED
@@ -7,13 +7,17 @@ require:
|
|
7
7
|
|
8
8
|
AllCops:
|
9
9
|
Exclude:
|
10
|
-
-
|
10
|
+
- lib/generators/auxiliary_rails/templates/**/*
|
11
|
+
- vendor/**/* # fix for CI
|
11
12
|
|
12
13
|
#################### Layout ##############################
|
13
14
|
|
14
|
-
Layout/
|
15
|
+
Layout/ArgumentAlignment:
|
15
16
|
EnforcedStyle: with_fixed_indentation
|
16
17
|
|
18
|
+
Layout/MultilineMethodCallIndentation:
|
19
|
+
EnforcedStyle: indented
|
20
|
+
|
17
21
|
#################### Metrics ##############################
|
18
22
|
|
19
23
|
Metrics/BlockLength:
|
@@ -22,6 +26,8 @@ Metrics/BlockLength:
|
|
22
26
|
|
23
27
|
#################### RSpec ################################
|
24
28
|
|
29
|
+
RSpec/ExampleLength:
|
30
|
+
Max: 10
|
25
31
|
|
26
32
|
RSpec/MultipleExpectations:
|
27
33
|
Max: 3
|
@@ -33,3 +39,6 @@ Style/Documentation:
|
|
33
39
|
|
34
40
|
Style/FrozenStringLiteralComment:
|
35
41
|
Enabled: false
|
42
|
+
|
43
|
+
Style/NegatedIf:
|
44
|
+
EnforcedStyle: postfix
|
data/.rubocop_todo.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on
|
3
|
+
# on 2020-01-25 02:14:48 +0200 using RuboCop version 0.78.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
@@ -8,20 +8,24 @@
|
|
8
8
|
|
9
9
|
# Offense count: 1
|
10
10
|
# Cop supports --auto-correct.
|
11
|
-
|
12
|
-
Layout/ExtraSpacing:
|
11
|
+
Lint/SendWithMixinArgument:
|
13
12
|
Exclude:
|
14
|
-
- 'auxiliary_rails.
|
13
|
+
- 'lib/auxiliary_rails/railtie.rb'
|
15
14
|
|
16
15
|
# Offense count: 1
|
17
|
-
|
18
|
-
|
19
|
-
Layout/SpaceAroundOperators:
|
20
|
-
Exclude:
|
21
|
-
- 'auxiliary_rails.gemspec'
|
16
|
+
RSpec/NestedGroups:
|
17
|
+
Max: 4
|
22
18
|
|
23
19
|
# Offense count: 1
|
24
20
|
# Configuration parameters: MinBodyLength.
|
25
21
|
Style/GuardClause:
|
26
22
|
Exclude:
|
27
23
|
- 'auxiliary_rails.gemspec'
|
24
|
+
|
25
|
+
# Offense count: 1
|
26
|
+
# Cop supports --auto-correct.
|
27
|
+
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
|
28
|
+
# SupportedStyles: slashes, percent_r, mixed
|
29
|
+
Style/RegexpLiteral:
|
30
|
+
Exclude:
|
31
|
+
- 'lib/generators/auxiliary_rails/command_generator.rb'
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,26 @@
|
|
1
1
|
# AuxiliaryRails Changelog
|
2
2
|
|
3
|
+
## v0.3.0
|
4
|
+
|
5
|
+
### Added
|
6
|
+
- Form Objects
|
7
|
+
- Query Objects
|
8
|
+
- Performable concern
|
9
|
+
- YARD docs and link to API documentation
|
10
|
+
- Commands usage examples
|
11
|
+
- Commands: `arguments` helper to get hash of all `param`s
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
- Namespace `Application` instead of `Abstract` prefixes for classes
|
15
|
+
- Commands migrate from `#call` to `#perform` method
|
16
|
+
- Commands: force validations
|
17
|
+
|
18
|
+
### Removed
|
19
|
+
- RuboCop generator replaced with `rubocop-ergoserv` gem
|
20
|
+
|
3
21
|
## v0.2.0
|
4
22
|
|
5
23
|
* Commands migration to `dry-initializer`
|
6
|
-
* Commands usage examples
|
7
24
|
|
8
25
|
## v0.1.7
|
9
26
|
|
@@ -26,7 +43,7 @@
|
|
26
43
|
|
27
44
|
## v0.1.3
|
28
45
|
|
29
|
-
*
|
46
|
+
* Upgrade `rubocop` gem and its configs
|
30
47
|
* `rubocop-rspec` and `rubocop-performance` gem iteration
|
31
48
|
* Added basic Rails application template
|
32
49
|
* CLI with `create_rails_app` command
|
data/CONTRIBUTING.md
CHANGED
@@ -1,7 +1 @@
|
|
1
1
|
# Contributing to AuxiliaryRails
|
2
|
-
|
3
|
-
## Rubocop Templates
|
4
|
-
|
5
|
-
### Versioning
|
6
|
-
|
7
|
-
File `rubocop_auxiliary_rails_template.yml` contains a basic template of Rubocop config. After making any changes to this file the line `Version: N` should be updated and `N` (version number) should be incremented by one.
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
auxiliary_rails (0.
|
4
|
+
auxiliary_rails (0.3.0)
|
5
|
+
dry-core
|
6
|
+
dry-initializer
|
5
7
|
dry-initializer-rails
|
6
8
|
thor
|
7
9
|
|
@@ -66,17 +68,19 @@ GEM
|
|
66
68
|
ast (2.4.0)
|
67
69
|
builder (3.2.4)
|
68
70
|
coderay (1.1.2)
|
69
|
-
concurrent-ruby (1.1.
|
70
|
-
crass (1.0.
|
71
|
+
concurrent-ruby (1.1.6)
|
72
|
+
crass (1.0.6)
|
71
73
|
diff-lcs (1.3)
|
72
|
-
dry-
|
74
|
+
dry-core (0.4.9)
|
75
|
+
concurrent-ruby (~> 1.0)
|
76
|
+
dry-initializer (3.0.3)
|
73
77
|
dry-initializer-rails (3.1.1)
|
74
78
|
dry-initializer (>= 2.4, < 4)
|
75
79
|
rails (> 3.0)
|
76
80
|
erubi (1.9.0)
|
77
81
|
globalid (0.4.2)
|
78
82
|
activesupport (>= 4.2.0)
|
79
|
-
i18n (1.
|
83
|
+
i18n (1.8.2)
|
80
84
|
concurrent-ruby (~> 1.0)
|
81
85
|
jaro_winkler (1.5.4)
|
82
86
|
loofah (2.4.0)
|
@@ -87,20 +91,20 @@ GEM
|
|
87
91
|
marcel (0.3.3)
|
88
92
|
mimemagic (~> 0.3.2)
|
89
93
|
method_source (0.9.2)
|
90
|
-
mimemagic (0.3.
|
94
|
+
mimemagic (0.3.4)
|
91
95
|
mini_mime (1.0.2)
|
92
96
|
mini_portile2 (2.4.0)
|
93
|
-
minitest (5.
|
97
|
+
minitest (5.14.0)
|
94
98
|
nio4r (2.5.2)
|
95
|
-
nokogiri (1.10.
|
99
|
+
nokogiri (1.10.9)
|
96
100
|
mini_portile2 (~> 2.4.0)
|
97
101
|
parallel (1.19.1)
|
98
|
-
parser (2.7.0.
|
102
|
+
parser (2.7.0.4)
|
99
103
|
ast (~> 2.4.0)
|
100
104
|
pry (0.12.2)
|
101
105
|
coderay (~> 1.1.0)
|
102
106
|
method_source (~> 0.9.0)
|
103
|
-
rack (2.
|
107
|
+
rack (2.2.2)
|
104
108
|
rack-test (1.1.0)
|
105
109
|
rack (>= 1.0, < 3)
|
106
110
|
rails (6.0.2.1)
|
@@ -144,16 +148,16 @@ GEM
|
|
144
148
|
diff-lcs (>= 1.2.0, < 2.0)
|
145
149
|
rspec-support (~> 3.9.0)
|
146
150
|
rspec-support (3.9.2)
|
147
|
-
rubocop (0.
|
151
|
+
rubocop (0.79.0)
|
148
152
|
jaro_winkler (~> 1.5.1)
|
149
153
|
parallel (~> 1.10)
|
150
|
-
parser (>= 2.
|
154
|
+
parser (>= 2.7.0.1)
|
151
155
|
rainbow (>= 2.2.2, < 4.0)
|
152
156
|
ruby-progressbar (~> 1.7)
|
153
157
|
unicode-display_width (>= 1.4.0, < 1.7)
|
154
158
|
rubocop-performance (1.5.2)
|
155
159
|
rubocop (>= 0.71.0)
|
156
|
-
rubocop-rspec (1.
|
160
|
+
rubocop-rspec (1.38.1)
|
157
161
|
rubocop (>= 0.68.1)
|
158
162
|
ruby-progressbar (1.10.1)
|
159
163
|
sprockets (4.0.0)
|
@@ -167,11 +171,11 @@ GEM
|
|
167
171
|
thread_safe (0.3.6)
|
168
172
|
tzinfo (1.2.6)
|
169
173
|
thread_safe (~> 0.1)
|
170
|
-
unicode-display_width (1.6.
|
174
|
+
unicode-display_width (1.6.1)
|
171
175
|
websocket-driver (0.7.1)
|
172
176
|
websocket-extensions (>= 0.1.0)
|
173
177
|
websocket-extensions (0.1.4)
|
174
|
-
zeitwerk (2.
|
178
|
+
zeitwerk (2.3.0)
|
175
179
|
|
176
180
|
PLATFORMS
|
177
181
|
ruby
|
@@ -183,7 +187,7 @@ DEPENDENCIES
|
|
183
187
|
rails (>= 5.2, < 7)
|
184
188
|
rake
|
185
189
|
rspec (~> 3.8)
|
186
|
-
rubocop
|
190
|
+
rubocop (= 0.79)
|
187
191
|
rubocop-performance
|
188
192
|
rubocop-rspec
|
189
193
|
|
data/README.md
CHANGED
@@ -29,18 +29,22 @@ Or install it yourself as:
|
|
29
29
|
|
30
30
|
## Usage
|
31
31
|
|
32
|
+
- [API documentation](https://www.rubydoc.info/gems/auxiliary_rails)
|
33
|
+
|
32
34
|
### Rails Application Templates
|
33
35
|
|
34
36
|
Install gem into the system (e.g. using `gem install auxiliary_rails`) then:
|
35
37
|
|
36
38
|
```sh
|
37
39
|
auxiliary_rails new APP_PATH
|
40
|
+
# or add `--develop` option to pull the most recent template from repository
|
41
|
+
auxiliary_rails new APP_PATH --develop
|
38
42
|
```
|
39
43
|
|
40
44
|
Or use `rails new` command specifying `--template` argument:
|
41
45
|
|
42
46
|
```sh
|
43
|
-
rails new APP_PATH --
|
47
|
+
rails new APP_PATH --database=postgresql --template=https://raw.githubusercontent.com/ergoserv/auxiliary_rails/develop/templates/rails/elementary.rb --skip-action-cable --skip-coffee --skip-test --skip-webpack-install
|
44
48
|
```
|
45
49
|
|
46
50
|
### Generators
|
@@ -52,8 +56,6 @@ rails generate auxiliary_rails:install
|
|
52
56
|
# Install one by one
|
53
57
|
rails generate auxiliary_rails:install_commands
|
54
58
|
rails generate auxiliary_rails:install_errors
|
55
|
-
rails generate auxiliary_rails:install_rubocop
|
56
|
-
rails generate auxiliary_rails:install_rubocop --no-specify-gems
|
57
59
|
|
58
60
|
# API resource generator
|
59
61
|
rails generate auxiliary_rails:api_resource
|
@@ -62,6 +64,205 @@ rails generate auxiliary_rails:api_resource
|
|
62
64
|
rails generate auxiliary_rails:command
|
63
65
|
```
|
64
66
|
|
67
|
+
### Command Objects
|
68
|
+
|
69
|
+
Variation of implementation of [Command pattern](https://en.wikipedia.org/wiki/Command_pattern).
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# app/commands/application_command.rb
|
73
|
+
class ApplicationCommand < AuxiliaryRails::Application::Command
|
74
|
+
end
|
75
|
+
|
76
|
+
# app/commands/register_user_command.rb
|
77
|
+
class RegisterUserCommand < ApplicationCommand
|
78
|
+
# Define command arguments
|
79
|
+
# using `param` or `option` methods provided by dry-initializer
|
80
|
+
# https://dry-rb.org/gems/dry-initializer/3.0/
|
81
|
+
param :email
|
82
|
+
param :password
|
83
|
+
|
84
|
+
# Define the results of the command
|
85
|
+
# using `attr_reader` and set it as a regular instance var inside the command
|
86
|
+
attr_reader :user
|
87
|
+
|
88
|
+
# Regular Active Model Validations can be used to validate params
|
89
|
+
# https://api.rubyonrails.org/classes/ActiveModel/Validations.html
|
90
|
+
# Use #valid?, #invalid?, #validate! methods to engage validations
|
91
|
+
validates :password, length: { in: 8..32 }
|
92
|
+
|
93
|
+
# Define the only public method `#perform`
|
94
|
+
# where command's flow is defined
|
95
|
+
def perform
|
96
|
+
# Use `return failure!` to exit from the command with failure
|
97
|
+
return failure! if registration_disabled?
|
98
|
+
|
99
|
+
# Method `#transaction` is a shortcut for `ActiveRecord::Base.transaction`
|
100
|
+
transaction do
|
101
|
+
# Keep the `#perform` method short and clean, put all the steps, actions
|
102
|
+
# and business logic into meaningful and self-explanatory methods
|
103
|
+
create_user
|
104
|
+
|
105
|
+
# Use `error!` method to interrupt the flow raising an error
|
106
|
+
error! unless @user.persistent?
|
107
|
+
|
108
|
+
send_notification
|
109
|
+
# ...
|
110
|
+
end
|
111
|
+
|
112
|
+
# Always end the `#perform` method with `success!`
|
113
|
+
# this will set the proper status and allow to chain command methods.
|
114
|
+
success!
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def create_user
|
120
|
+
@user = User.create(email: email, password: password)
|
121
|
+
end
|
122
|
+
|
123
|
+
def send_notification
|
124
|
+
# ...
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
### usage ###
|
129
|
+
|
130
|
+
class RegistrationsController
|
131
|
+
def register
|
132
|
+
cmd = RegisterUserCommand.call(params[:email], params[:password])
|
133
|
+
|
134
|
+
if cmd.success?
|
135
|
+
redirect_to user_path(cmd.user) and return
|
136
|
+
else
|
137
|
+
@errors = cmd.errors
|
138
|
+
end
|
139
|
+
|
140
|
+
### OR ###
|
141
|
+
|
142
|
+
RegisterUserCommand.call(params[:email], params[:password])
|
143
|
+
.on(:success) do
|
144
|
+
redirect_to dashboard_path and return
|
145
|
+
end
|
146
|
+
.on(:failure) do |cmd|
|
147
|
+
@errors = cmd.errors
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
### Form Objects
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
# app/forms/application_form.rb
|
157
|
+
class ApplicationForm < AuxiliaryRails::Application::Form
|
158
|
+
end
|
159
|
+
|
160
|
+
# app/forms/company_registration_form.rb
|
161
|
+
class CompanyRegistrationForm < ApplicationForm
|
162
|
+
# Define form attributes
|
163
|
+
attribute :company_name, :string
|
164
|
+
attribute :email, :string
|
165
|
+
|
166
|
+
# Define form submission results
|
167
|
+
attr_reader :company
|
168
|
+
|
169
|
+
# Regular Active Model Validations can be used to validate attributes
|
170
|
+
# https://api.rubyonrails.org/classes/ActiveModel/Validations.html
|
171
|
+
validates :company_name, presence: true
|
172
|
+
validates :email, email: true
|
173
|
+
|
174
|
+
def perform
|
175
|
+
# Perform business logic here
|
176
|
+
|
177
|
+
# Use `attr_reader` to expose the submission results.
|
178
|
+
@company = create_company
|
179
|
+
# Return `failure!` to indicate failure and stop execution
|
180
|
+
return failure! if @company.invalid?
|
181
|
+
|
182
|
+
send_notification if email.present?
|
183
|
+
|
184
|
+
# Always end with `success!` method call to indicate success
|
185
|
+
success!
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
def create_comany
|
191
|
+
Company.create(name: company_name)
|
192
|
+
end
|
193
|
+
|
194
|
+
def send_notification
|
195
|
+
# mail to: email
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
### Usage ###
|
200
|
+
|
201
|
+
form = CompanyRegistrationForm.call(params[:company])
|
202
|
+
if form.success?
|
203
|
+
redirect_to company_path(form.company) and return
|
204
|
+
else
|
205
|
+
@errors = form.errors
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
### Query Objects
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
# app/queries/application_query.rb
|
213
|
+
class ApplicationQuery < AuxiliaryRails::Application::Query
|
214
|
+
end
|
215
|
+
|
216
|
+
# app/queries/authors_query.rb
|
217
|
+
class AuthorsQuery < ApplicationQuery
|
218
|
+
default_relation Author.all
|
219
|
+
|
220
|
+
option :name_like, optional: true
|
221
|
+
option :with_books, optional: true
|
222
|
+
|
223
|
+
def perform
|
224
|
+
if recent == true
|
225
|
+
# equivalent to `@query = @query.order(:created_at)`:
|
226
|
+
query order(:created_at)
|
227
|
+
end
|
228
|
+
|
229
|
+
if name_like.present?
|
230
|
+
query with_name_like(name_like)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
def with_name_like(value)
|
237
|
+
where('authors.name LIKE ?', "%#{value}%")
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# app/queries/authors_with_books_query.rb
|
242
|
+
class AuthorsWithBooksQuery < AuthorsQuery
|
243
|
+
option :min_book_count, default: { 3 }
|
244
|
+
|
245
|
+
def perform
|
246
|
+
query joins(:books)
|
247
|
+
.group(:author_id)
|
248
|
+
.having('COUNT(books.id) > ?', min_book_count)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
### Usage ###
|
253
|
+
|
254
|
+
# it is possible to wrap query object in a scope and use as a regular scope
|
255
|
+
# app/models/inmate.rb
|
256
|
+
class Author < ApplicationRecord
|
257
|
+
scope :name_like, ->(value) { AuthorsQuery.call(name_like: value) }
|
258
|
+
end
|
259
|
+
|
260
|
+
authors = Author.name_like('Arthur')
|
261
|
+
|
262
|
+
# or call query directly
|
263
|
+
authors = AuthorsWithBooksQuery.call(min_book_count: 10)
|
264
|
+
```
|
265
|
+
|
65
266
|
### View Helpers
|
66
267
|
|
67
268
|
```ruby
|