cafe_car 0.1.0 → 0.1.2
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/README.md +682 -8
- data/Rakefile +21 -0
- data/app/assets/fonts/Lexend.css +7 -0
- data/app/assets/fonts/Lexend.ttf +0 -0
- data/app/assets/images/noise.svg +16 -0
- data/app/assets/stylesheets/actiontext.css +31 -0
- data/app/assets/stylesheets/application.css +1 -0
- data/app/assets/stylesheets/cafe_car/code/base16-dark.css +89 -0
- data/app/assets/stylesheets/cafe_car/code/base16-light.css +90 -0
- data/app/assets/stylesheets/cafe_car/pagination.css +5 -0
- data/app/assets/stylesheets/cafe_car/themes/cool.css +32 -0
- data/app/assets/stylesheets/cafe_car/themes/cool2.css +31 -0
- data/app/assets/stylesheets/cafe_car/themes/defaults.css +60 -0
- data/app/assets/stylesheets/cafe_car/themes/warm-dark.css +29 -0
- data/app/assets/stylesheets/cafe_car/themes/warm.css +24 -0
- data/app/assets/stylesheets/cafe_car/tooltips.css +20 -0
- data/app/assets/stylesheets/cafe_car/trix.css +56 -0
- data/app/assets/stylesheets/cafe_car/utility.css +63 -0
- data/app/assets/stylesheets/cafe_car.css +96 -0
- data/app/assets/stylesheets/iconoir.css +22 -0
- data/app/assets/stylesheets/ui/Alert.css +25 -0
- data/app/assets/stylesheets/ui/Article.css +11 -0
- data/app/assets/stylesheets/ui/Button.css +42 -0
- data/app/assets/stylesheets/ui/Card.css +74 -0
- data/app/assets/stylesheets/ui/Chat.css +33 -0
- data/app/assets/stylesheets/ui/Close.css +11 -0
- data/app/assets/stylesheets/ui/Code.css +4 -0
- data/app/assets/stylesheets/ui/Controls.css +16 -0
- data/app/assets/stylesheets/ui/Error.css +3 -0
- data/app/assets/stylesheets/ui/Example.css +45 -0
- data/app/assets/stylesheets/ui/Field.css +31 -0
- data/app/assets/stylesheets/ui/Grid.css +6 -0
- data/app/assets/stylesheets/ui/Group.css +16 -0
- data/app/assets/stylesheets/ui/Icon.css +27 -0
- data/app/assets/stylesheets/ui/Image.css +14 -0
- data/app/assets/stylesheets/ui/InfoCircle.css +11 -0
- data/app/assets/stylesheets/ui/Input.css +36 -0
- data/app/assets/stylesheets/ui/Layout.css +100 -0
- data/app/assets/stylesheets/ui/Menu.css +38 -0
- data/app/assets/stylesheets/ui/Modal.css +26 -0
- data/app/assets/stylesheets/ui/Navigation.css +37 -0
- data/app/assets/stylesheets/ui/Page.css +105 -0
- data/app/assets/stylesheets/ui/Row.css +9 -0
- data/app/assets/stylesheets/ui/Table.css +101 -0
- data/app/assets/stylesheets/ui/components.css +24 -0
- data/app/controllers/cafe_car/application_controller.rb +9 -0
- data/app/controllers/cafe_car/examples_controller.rb +22 -0
- data/app/controllers/cafe_car/sessions_controller.rb +30 -0
- data/app/controllers/concerns/cafe_car/authentication.rb +61 -0
- data/app/javascript/application.js +5 -0
- data/app/javascript/cafe_car.js +174 -0
- data/app/models/cafe_car/session.rb +18 -0
- data/app/policies/cafe_car/application_policy.rb +42 -0
- data/app/policies/cafe_car/session_policy.rb +19 -0
- data/app/presenters/cafe_car/action_text/rich_text_presenter.rb +7 -0
- data/app/presenters/cafe_car/active_record/base_presenter.rb +6 -0
- data/app/presenters/cafe_car/active_record/relation_presenter.rb +17 -0
- data/app/presenters/cafe_car/active_storage/attached/one_presenter.rb +9 -0
- data/app/presenters/cafe_car/active_storage/attachment_presenter.rb +18 -0
- data/app/presenters/cafe_car/basic_object_presenter.rb +5 -0
- data/app/presenters/cafe_car/code_presenter.rb +18 -0
- data/app/presenters/cafe_car/currency_presenter.rb +5 -0
- data/app/presenters/cafe_car/date_and_time/compatibility_presenter.rb +6 -0
- data/app/presenters/cafe_car/date_presenter.rb +5 -0
- data/app/presenters/cafe_car/date_time_presenter.rb +11 -0
- data/app/presenters/cafe_car/enumerable_presenter.rb +13 -0
- data/app/presenters/cafe_car/false_class_presenter.rb +5 -0
- data/app/presenters/cafe_car/hash_presenter.rb +6 -0
- data/app/presenters/cafe_car/nil_class_presenter.rb +13 -0
- data/app/presenters/cafe_car/presenter.rb +157 -0
- data/app/presenters/cafe_car/range_presenter.rb +16 -0
- data/app/presenters/cafe_car/record_presenter.rb +5 -0
- data/app/presenters/cafe_car/string_presenter.rb +20 -0
- data/app/presenters/cafe_car/symbol_presenter.rb +5 -0
- data/app/presenters/cafe_car/true_class_presenter.rb +5 -0
- data/app/ui/cafe_car/ui/button.rb +9 -0
- data/app/ui/cafe_car/ui/card.rb +18 -0
- data/app/ui/cafe_car/ui/field.rb +11 -0
- data/app/ui/cafe_car/ui/grid.rb +30 -0
- data/app/ui/cafe_car/ui/layout.rb +7 -0
- data/app/ui/cafe_car/ui/page.rb +14 -0
- data/app/views/application/_actions.html.haml +1 -0
- data/app/views/application/_alerts.html.haml +2 -0
- data/app/views/application/_body.html.haml +7 -0
- data/app/views/application/_controls.html.haml +12 -0
- data/app/views/application/_debug.html.haml +18 -0
- data/app/views/application/_empty.html.haml +1 -0
- data/app/views/application/_errors.html.haml +4 -0
- data/app/views/application/_field.html.haml +5 -0
- data/app/views/application/_fields.html.haml +1 -0
- data/app/views/application/_filters.html.haml +8 -0
- data/app/views/application/_form.html.haml +6 -0
- data/app/views/application/_grid.html.haml +3 -0
- data/app/views/application/_grid_item.html.haml +1 -0
- data/app/views/application/_head.html.haml +17 -0
- data/app/views/application/_index.html.haml +8 -0
- data/app/views/application/_index_actions.html.haml +7 -0
- data/app/views/application/_navigation.html.haml +9 -0
- data/app/views/application/_navigation_links.html.haml +5 -0
- data/app/views/application/_notes.html.haml +10 -0
- data/app/views/application/_popup.html.haml +7 -0
- data/app/views/application/_show.html.haml +9 -0
- data/app/views/application/_submit.html.haml +1 -0
- data/app/views/application/_table.html.haml +6 -0
- data/app/views/cafe_car/application/create.turbo_stream.haml +2 -0
- data/app/views/cafe_car/application/destroy.turbo_stream.haml +1 -0
- data/app/views/cafe_car/application/edit.html.haml +18 -0
- data/app/views/cafe_car/application/edit.turbo_stream.haml +7 -0
- data/app/views/cafe_car/application/index.html.haml +15 -0
- data/app/views/cafe_car/application/new.html.haml +8 -0
- data/app/views/cafe_car/application/new.turbo_stream.haml +8 -0
- data/app/views/cafe_car/application/show.html.haml +36 -0
- data/app/views/cafe_car/application/update.turbo_stream.haml +2 -0
- data/app/views/cafe_car/examples/_example.html.haml +12 -0
- data/app/views/cafe_car/examples/_index.html.haml +16 -0
- data/app/views/cafe_car/examples/_navigation_links.html.haml +2 -0
- data/app/views/cafe_car/examples/ui/_alert.html.haml +4 -0
- data/app/views/cafe_car/examples/ui/_button.html.haml +3 -0
- data/app/views/cafe_car/examples/ui/_card.html.haml +6 -0
- data/app/views/cafe_car/examples/ui/_chat.html.haml +3 -0
- data/app/views/cafe_car/examples/ui/_controls.html.haml +3 -0
- data/app/views/cafe_car/examples/ui/_error.html.haml +1 -0
- data/app/views/cafe_car/examples/ui/_field.html.haml +9 -0
- data/app/views/cafe_car/examples/ui/_grid.html.haml +11 -0
- data/app/views/cafe_car/examples/ui/_group.html.haml +21 -0
- data/app/views/cafe_car/examples/ui/_info_circle.html.haml +1 -0
- data/app/views/cafe_car/examples/ui/_menu.html.haml +5 -0
- data/app/views/cafe_car/examples/ui/_modal.html.haml +4 -0
- data/app/views/cafe_car/examples/ui/_navigation.html.haml +4 -0
- data/app/views/cafe_car/examples/ui/_page.html.haml +4 -0
- data/app/views/cafe_car/examples/ui/_table.html.haml +13 -0
- data/app/views/cafe_car/layouts/mailer.html.haml +8 -0
- data/app/views/cafe_car/layouts/mailer.text.erb +1 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/layouts/application.html.haml +4 -0
- data/app/views/layouts/mailer.html.erb +13 -0
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/notes/_fields.html.haml +1 -0
- data/app/views/passwords_mailer/reset.html.haml +5 -0
- data/app/views/passwords_mailer/reset.text.erb +4 -0
- data/app/views/ui/_card.html.haml +8 -0
- data/app/views/ui/_field.html.haml +1 -0
- data/app/views/ui/_modal_close.html.haml +1 -0
- data/app/views/ui/_page.html.haml +7 -0
- data/config/brakeman.ignore +77 -0
- data/config/importmap.rb +12 -0
- data/config/locales/en.yml +63 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20251005220017_create_slugs.rb +13 -0
- data/lib/cafe_car/active_record.rb +21 -0
- data/lib/cafe_car/application_responder.rb +17 -0
- data/lib/cafe_car/attributes.rb +23 -0
- data/lib/cafe_car/auto_resolver.rb +49 -0
- data/lib/cafe_car/caching.rb +20 -0
- data/lib/cafe_car/component.rb +155 -0
- data/lib/cafe_car/context.rb +17 -0
- data/lib/cafe_car/controller/filtering.rb +30 -0
- data/lib/cafe_car/controller.rb +218 -0
- data/lib/cafe_car/core_ext/array.rb +24 -0
- data/lib/cafe_car/core_ext/hash.rb +15 -0
- data/lib/cafe_car/core_ext/module.rb +15 -0
- data/lib/cafe_car/core_ext.rb +5 -0
- data/lib/cafe_car/current.rb +9 -0
- data/lib/cafe_car/engine.rb +107 -0
- data/lib/cafe_car/field_builder.rb +44 -0
- data/lib/cafe_car/field_info.rb +144 -0
- data/lib/cafe_car/fields.rb +21 -0
- data/lib/cafe_car/filter/field_builder.rb +4 -0
- data/lib/cafe_car/filter/field_info.rb +22 -0
- data/lib/cafe_car/filter/form_builder.rb +21 -0
- data/lib/cafe_car/filter.rb +5 -0
- data/lib/cafe_car/filter_builder.rb +20 -0
- data/lib/cafe_car/form_builder.rb +105 -0
- data/lib/cafe_car/generators.rb +30 -0
- data/lib/cafe_car/helpers.rb +178 -0
- data/lib/cafe_car/href_builder.rb +97 -0
- data/lib/cafe_car/informable.rb +9 -0
- data/lib/cafe_car/input_builder.rb +25 -0
- data/lib/cafe_car/inputs/association_builder.rb +6 -0
- data/lib/cafe_car/inputs/base_input.rb +19 -0
- data/lib/cafe_car/inputs/belongs_to_builder.rb +6 -0
- data/lib/cafe_car/inputs/password_input.rb +7 -0
- data/lib/cafe_car/inputs/string_input.rb +7 -0
- data/lib/cafe_car/link_builder.rb +65 -0
- data/lib/cafe_car/model.rb +23 -0
- data/lib/cafe_car/model_info.rb +24 -0
- data/lib/cafe_car/name_patch.rb +17 -0
- data/lib/cafe_car/navigation.rb +76 -0
- data/lib/cafe_car/option_helpers.rb +53 -0
- data/lib/cafe_car/param_parser.rb +45 -0
- data/lib/cafe_car/pluralization.rb +15 -0
- data/lib/cafe_car/policy.rb +77 -0
- data/lib/cafe_car/proc_helpers.rb +13 -0
- data/lib/cafe_car/query_builder.rb +186 -0
- data/lib/cafe_car/queryable.rb +29 -0
- data/lib/cafe_car/resolver.rb +27 -0
- data/lib/cafe_car/routing.rb +17 -0
- data/lib/cafe_car/table/body_builder.rb +12 -0
- data/lib/cafe_car/table/builder.rb +52 -0
- data/lib/cafe_car/table/foot_builder.rb +14 -0
- data/lib/cafe_car/table/head_builder.rb +26 -0
- data/lib/cafe_car/table/label_builder.rb +48 -0
- data/lib/cafe_car/table/objects_builder.rb +8 -0
- data/lib/cafe_car/table/row_builder.rb +39 -0
- data/lib/cafe_car/table_builder.rb +13 -0
- data/lib/cafe_car/turbo_tag_builder.rb +7 -0
- data/lib/cafe_car/ui.rb +11 -0
- data/lib/cafe_car/version.rb +1 -1
- data/lib/cafe_car/visitors.rb +21 -0
- data/lib/cafe_car.rb +25 -168
- data/lib/generators/cafe_car/controller/USAGE +11 -0
- data/lib/generators/cafe_car/controller/controller_generator.rb +26 -0
- data/lib/generators/cafe_car/controller/templates/controller.rb.tt +5 -0
- data/lib/generators/cafe_car/install/USAGE +8 -0
- data/lib/generators/cafe_car/install/install_generator.rb +46 -0
- data/lib/generators/cafe_car/install/templates/application_policy.rb.tt +7 -0
- data/lib/generators/cafe_car/notes/USAGE +12 -0
- data/lib/generators/cafe_car/notes/notes_generator.rb +13 -0
- data/lib/generators/cafe_car/notes/templates/create_notes.rb.tt +12 -0
- data/lib/generators/cafe_car/notes/templates/notable.rb.tt +7 -0
- data/lib/generators/cafe_car/notes/templates/note.rb.tt +6 -0
- data/lib/generators/cafe_car/policy/USAGE +8 -0
- data/lib/generators/cafe_car/policy/policy_generator.rb +39 -0
- data/lib/generators/cafe_car/policy/templates/policy.rb.tt +20 -0
- data/lib/generators/cafe_car/resource/USAGE +13 -0
- data/lib/generators/cafe_car/resource/resource_generator.rb +32 -0
- data/lib/generators/cafe_car/sessions/USAGE +17 -0
- data/lib/generators/cafe_car/sessions/sessions_generator.rb +29 -0
- data/lib/generators/cafe_car/sessions/templates/create_sessions.rb.tt +12 -0
- data/lib/tasks/holdco_tasks.rake +532 -0
- data/lib/tasks/templates/tasks_header.md +37 -0
- metadata +444 -21
- data/app/views/cafe_car/application/_fields.html.erb +0 -7
- data/app/views/cafe_car/application/_filters.html.erb +0 -0
- data/app/views/cafe_car/application/_form.html.erb +0 -22
- data/lib/cafe_car/railtie.rb +0 -4
- /data/app/views/{cafe_car/application/_actions.html.erb → application/_aside.html.haml} +0 -0
- /data/app/views/{cafe_car/application/_aside.html.erb → application/_footer.html.haml} +0 -0
- /data/app/views/cafe_car/{application/_extra_fields.html.erb → examples/_index_actions.html.haml} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fdfdd44f9f4f564e6fe25b17a694b0dd6dce990efdc30990d0cef474a98cc718
|
|
4
|
+
data.tar.gz: e483b2317f3a39b3b7c57b9c44bd20e3e766376f4889551bdc8ed199b8e6db52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: daba4f7613586a3ff4f74e952800d3738965afafb81fbf1bfe82eec4b3ae5363c99d7aa879c1d3be438ff1ecdbf18d37d071307072487a1d2fb434dbfa3a6b62
|
|
7
|
+
data.tar.gz: '051968fadd32602c6b8884443200f030aeae9bcecb731c602161b854f0222e8197b65d568eadce9e1c45175cc66b64fab899d74f5df367df856f7c39e29bc8eb'
|
data/README.md
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
# CafeCar
|
|
2
|
-
Short description and motivation.
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
[](https://github.com/craft-concept/cafe_car/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/cafe_car)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
CafeCar is a Rails engine that extends the MVC "view" layer to provide automatic
|
|
8
|
+
CRUD UI generation with sensible defaults. Its philosophy is rooted in the idea
|
|
9
|
+
that Rails should render _something_ that represents the CRUD operations of your
|
|
10
|
+
models by default. These defaults can then be expanded or overridden on either
|
|
11
|
+
an application-wide or model-specific basis.
|
|
12
|
+
|
|
13
|
+
**Perfect for**: Admin panels, internal tools, and rapid prototyping.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- 🚀 **Auto-generated CRUD interfaces** - One line of code generates complete
|
|
18
|
+
index, show, new, edit views
|
|
19
|
+
- 🎨 **Component-based UI system** - Flexible, composable components for
|
|
20
|
+
building interfaces
|
|
21
|
+
- 🔐 **Built-in authorization** - Pundit integration for attribute-level
|
|
22
|
+
permissions
|
|
23
|
+
- 📊 **Smart presenters** - Automatic type-aware display of your data
|
|
24
|
+
- 🔍 **Advanced filtering** - Range queries, comparison operators, and
|
|
25
|
+
association filters
|
|
26
|
+
- 📄 **Pagination & sorting** - Kaminari integration with sortable columns
|
|
27
|
+
- ⚡ **Hotwire ready** - Turbo Streams support out of the box
|
|
28
|
+
- 📝 **Intelligent forms** - Auto-generated forms with smart field detection
|
|
29
|
+
|
|
30
|
+
## Prerequisites
|
|
31
|
+
|
|
32
|
+
- Ruby 3.3+ (developed and tested against 3.3.5)
|
|
33
|
+
- Rails 8.0+ (developed and tested against Rails 8.1)
|
|
6
34
|
|
|
7
35
|
## Installation
|
|
36
|
+
|
|
8
37
|
Add this line to your application's Gemfile:
|
|
9
38
|
|
|
10
39
|
```ruby
|
|
@@ -12,17 +41,662 @@ gem "cafe_car"
|
|
|
12
41
|
```
|
|
13
42
|
|
|
14
43
|
And then execute:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
$ bundle install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run the installer to set up CafeCar in your application:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
$ rails generate cafe_car:install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This will:
|
|
56
|
+
|
|
57
|
+
- Add required gems (cnc, bcrypt, paper_trail, factory_bot_rails, faker, rouge)
|
|
58
|
+
plus development tools (hotwire-livereload, better_errors, binding_of_caller,
|
|
59
|
+
chrome_devtools_rails, i18n-debug)
|
|
60
|
+
- Mount the CafeCar engine at `/` under the `:admin` namespace
|
|
61
|
+
- Create `app/policies/application_policy.rb`
|
|
62
|
+
- Add `CafeCar::Controller` to your `ApplicationController`
|
|
63
|
+
- Set up JavaScript imports for CafeCar, Trix, and ActionText
|
|
64
|
+
|
|
65
|
+
## Getting Started
|
|
66
|
+
|
|
67
|
+
### Quick Start: Generate a Complete Resource
|
|
68
|
+
|
|
69
|
+
The fastest way to get started is to generate a complete resource (model +
|
|
70
|
+
controller + policy):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
$ rails generate cafe_car:resource Product name:string price:decimal description:text
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This creates:
|
|
77
|
+
|
|
78
|
+
- Migration and model (`app/models/product.rb`)
|
|
79
|
+
- Controller with CRUD actions (`app/controllers/products_controller.rb`)
|
|
80
|
+
- Policy with permission methods (`app/policies/product_policy.rb`)
|
|
81
|
+
|
|
82
|
+
Run migrations and start your server:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
$ rails db:migrate
|
|
86
|
+
$ rails server
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Navigate to `/products` and you'll see a fully functional CRUD interface!
|
|
90
|
+
|
|
91
|
+
### Manual Setup
|
|
92
|
+
|
|
93
|
+
You can also add CafeCar to existing resources:
|
|
94
|
+
|
|
95
|
+
#### 1. Add to Controller
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class ProductsController < ApplicationController
|
|
99
|
+
cafe_car
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
That single line provides:
|
|
104
|
+
|
|
105
|
+
- All 7 RESTful actions (index, show, new, create, edit, update, destroy)
|
|
106
|
+
- Automatic authorization via Pundit
|
|
107
|
+
- Filtering and sorting
|
|
108
|
+
- JSON/HTML/Turbo Stream responses
|
|
109
|
+
- Smart parameter handling
|
|
110
|
+
|
|
111
|
+
#### 2. Create a Policy
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# app/policies/product_policy.rb
|
|
115
|
+
class ProductPolicy < ApplicationPolicy
|
|
116
|
+
def index? = user.present?
|
|
117
|
+
def show? = user.present?
|
|
118
|
+
def create? = user.admin?
|
|
119
|
+
def update? = user.admin?
|
|
120
|
+
def destroy? = user.admin?
|
|
121
|
+
|
|
122
|
+
def permitted_attributes
|
|
123
|
+
[:name, :price, :description, :category_id]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The policy controls both authorization and which attributes can be edited.
|
|
129
|
+
|
|
130
|
+
## Core Components
|
|
131
|
+
|
|
132
|
+
### Controllers
|
|
133
|
+
|
|
134
|
+
The `CafeCar::Controller` module provides automatic CRUD functionality with the
|
|
135
|
+
`cafe_car` class method.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class Admin::ClientsController < ApplicationController
|
|
139
|
+
cafe_car
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**What you get:**
|
|
144
|
+
|
|
145
|
+
- **RESTful actions**: `index`, `show`, `new`, `edit`, `create`, `update`,
|
|
146
|
+
`destroy`
|
|
147
|
+
- **Authorization**: Automatic `authorize!` before each action
|
|
148
|
+
- **Smart defaults**: Model detection from controller name
|
|
149
|
+
- **Callbacks**: Lifecycle hooks for `render`, `update`, `create`, `destroy`
|
|
150
|
+
- **Responders**: JSON, HTML, and Turbo Stream responses
|
|
151
|
+
|
|
152
|
+
**Limiting actions:**
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
cafe_car only: [:index, :show]
|
|
156
|
+
# or
|
|
157
|
+
cafe_car except: [:destroy]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Custom model:**
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class Admin::ClientsController < ApplicationController
|
|
164
|
+
model Company # Use Company model instead of Client
|
|
165
|
+
cafe_car
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Callbacks:**
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
class ProductsController < ApplicationController
|
|
173
|
+
cafe_car
|
|
174
|
+
|
|
175
|
+
set_callback :create, :after do |controller|
|
|
176
|
+
NotificationMailer.product_created(controller.object).deliver_later
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Policies
|
|
182
|
+
|
|
183
|
+
CafeCar extends Pundit with attribute-level permissions and auto-detection of
|
|
184
|
+
displayable fields.
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
class ClientPolicy < ApplicationPolicy
|
|
188
|
+
def index? = admin?
|
|
189
|
+
def show? = admin?
|
|
190
|
+
def create? = admin?
|
|
191
|
+
def update? = admin?
|
|
192
|
+
def destroy? = update?
|
|
193
|
+
|
|
194
|
+
def permitted_attributes
|
|
195
|
+
[:name, :owner_id, :email, :phone]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
class Scope < Scope
|
|
199
|
+
def resolve
|
|
200
|
+
admin? ? scope.all : scope.where(owner: user)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Key methods:**
|
|
207
|
+
|
|
208
|
+
- `permitted_attributes` - Attributes that can be edited via forms
|
|
209
|
+
- `displayable_attributes` - Attributes shown in views (auto-detected from
|
|
210
|
+
columns + associations)
|
|
211
|
+
- `displayable_associations` - Associations that can be displayed
|
|
212
|
+
- `filtered_attribute?(attr)` - Check if attribute should be hidden (uses Rails
|
|
213
|
+
parameter filters)
|
|
214
|
+
|
|
215
|
+
**Scope pattern:**
|
|
216
|
+
|
|
217
|
+
The `Scope` class filters collections based on user permissions:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
class Scope < Scope
|
|
221
|
+
def resolve
|
|
222
|
+
admin? ? scope.all : scope.where(owner: user)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Presenters
|
|
228
|
+
|
|
229
|
+
Presenters convert model objects into view-ready representations with automatic
|
|
230
|
+
type detection.
|
|
231
|
+
|
|
232
|
+
**Automatic usage** (in views):
|
|
233
|
+
|
|
234
|
+
```erb
|
|
235
|
+
<%= present(@product) %>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This automatically:
|
|
239
|
+
|
|
240
|
+
1. Finds the appropriate presenter for the object type
|
|
241
|
+
2. Checks policy permissions
|
|
242
|
+
3. Renders displayable attributes
|
|
243
|
+
4. Uses type-specific formatting
|
|
244
|
+
|
|
245
|
+
**Custom presenters:**
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# app/presenters/product_presenter.rb
|
|
249
|
+
class ProductPresenter < CafeCar::Presenter
|
|
250
|
+
show :name
|
|
251
|
+
show :price
|
|
252
|
+
show :description
|
|
253
|
+
show :category
|
|
254
|
+
show :created_at
|
|
255
|
+
|
|
256
|
+
# Custom display method
|
|
257
|
+
def preview
|
|
258
|
+
"#{name} - #{format_currency(price)}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def format_currency(amount)
|
|
264
|
+
"$#{amount}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Built-in presenters:**
|
|
270
|
+
|
|
271
|
+
- `RecordPresenter` - ActiveRecord models
|
|
272
|
+
- `DatePresenter`, `DateTimePresenter` - Dates and times
|
|
273
|
+
- `CurrencyPresenter` - Money values
|
|
274
|
+
- `RangePresenter` - Range objects
|
|
275
|
+
- `ActiveStorage::AttachmentPresenter` - File attachments
|
|
276
|
+
- `ActionText::RichTextPresenter` - Rich text content
|
|
277
|
+
- `EnumerablePresenter`, `HashPresenter` - Collections
|
|
278
|
+
- `NilClassPresenter` - Handles nil values gracefully
|
|
279
|
+
|
|
280
|
+
**Presenter methods:**
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
presenter = present(@product)
|
|
284
|
+
presenter.show(:name) # Display single attribute
|
|
285
|
+
presenter.attributes # All displayable attributes
|
|
286
|
+
presenter.associations # All displayable associations
|
|
287
|
+
presenter.to_html # Render to HTML
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### UI Components
|
|
291
|
+
|
|
292
|
+
CafeCar provides a flexible component system for building interfaces.
|
|
293
|
+
|
|
294
|
+
**Basic usage:**
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
# In views or helpers
|
|
298
|
+
ui.Card do
|
|
299
|
+
ui.Field label: "Name" do
|
|
300
|
+
@product.name
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Available components:**
|
|
306
|
+
|
|
307
|
+
- `Page` - Page container with title and actions
|
|
308
|
+
- `Grid`, `Row` - Layout containers
|
|
309
|
+
- `Card` - Content cards
|
|
310
|
+
- `Table` - Data tables
|
|
311
|
+
- `Field` - Form fields with labels
|
|
312
|
+
- `Button` - Action buttons
|
|
313
|
+
- `Modal` - Modal dialogs
|
|
314
|
+
- `Alert` - Flash messages
|
|
315
|
+
- `Menu`, `Navigation` - Navigation elements
|
|
316
|
+
|
|
317
|
+
**Component options:**
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
ui.Button "Save", class: "primary", type: "submit"
|
|
321
|
+
ui.Field label: "Email", required: true, hint: "We'll never share this"
|
|
322
|
+
ui.Card title: "Details", collapsed: false
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Custom components:**
|
|
326
|
+
|
|
327
|
+
Create partials in `app/views/cafe_car/ui/`:
|
|
328
|
+
|
|
329
|
+
```haml
|
|
330
|
+
-# app/views/cafe_car/ui/_badge.html.haml
|
|
331
|
+
%span.badge{ class: ui.classname }
|
|
332
|
+
= yield
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Use it:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
ui.Badge class: "success" do
|
|
339
|
+
"Active"
|
|
340
|
+
end
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Forms
|
|
344
|
+
|
|
345
|
+
CafeCar provides an enhanced form builder with smart field detection.
|
|
346
|
+
|
|
347
|
+
**Basic forms:**
|
|
348
|
+
|
|
349
|
+
```erb
|
|
350
|
+
<%= form_with model: @product do |f| %>
|
|
351
|
+
<%= f.input :name %>
|
|
352
|
+
<%= f.input :price %>
|
|
353
|
+
<%= f.input :description, as: :text %>
|
|
354
|
+
<%= f.association :category %>
|
|
355
|
+
<%= f.submit %>
|
|
356
|
+
<% end %>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Smart field types:**
|
|
360
|
+
|
|
361
|
+
The form builder automatically detects field types:
|
|
362
|
+
|
|
363
|
+
- Password fields (columns named `password`, `password_confirmation`)
|
|
364
|
+
- File attachments (ActiveStorage `has_one_attached`, `has_many_attached`)
|
|
365
|
+
- Rich text (ActionText `has_rich_text`)
|
|
366
|
+
- Associations (belongs_to, has_many)
|
|
367
|
+
- Polymorphic associations
|
|
368
|
+
- Dates, datetimes, booleans, etc.
|
|
369
|
+
|
|
370
|
+
**Custom field rendering:**
|
|
371
|
+
|
|
372
|
+
```erb
|
|
373
|
+
<%= form_with model: @product do |f| %>
|
|
374
|
+
<%= f.field(:price).label %>
|
|
375
|
+
<%= f.field(:price).input class: "currency" %>
|
|
376
|
+
<%= f.field(:price).hint "In USD" %>
|
|
377
|
+
<%= f.field(:price).error %>
|
|
378
|
+
<% end %>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Association select:**
|
|
382
|
+
|
|
383
|
+
```erb
|
|
384
|
+
<%= f.association :category %>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Automatically creates a select dropdown with all categories.
|
|
388
|
+
|
|
389
|
+
### Filtering & Sorting
|
|
390
|
+
|
|
391
|
+
CafeCar provides advanced filtering with minimal configuration.
|
|
392
|
+
|
|
393
|
+
**URL-based filtering:**
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
/products?name=Widget&price.min=10&price.max=50&created_at=2024-01-01..2024-12-31
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Filter operators:**
|
|
400
|
+
|
|
401
|
+
- **Range queries**: `created_at=2024..2025-01-01`
|
|
402
|
+
- **Comparisons**: `price.min=10`, `price.max=50`
|
|
403
|
+
- **Greater than**: `price.gt=10` or `price=>10`
|
|
404
|
+
- **Less than**: `price.lt=50` or `price=<50`
|
|
405
|
+
- **Equals**: `status=active` or `status.eq=active`
|
|
406
|
+
- **Arrays**: `tags=red,blue,green`
|
|
407
|
+
|
|
408
|
+
**Sorting:**
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
/products?sort=name # Ascending
|
|
412
|
+
/products?sort=-price # Descending (note the minus)
|
|
413
|
+
/products?sort=category,-price # Multiple columns
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**In models:**
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
class Product < ApplicationRecord
|
|
420
|
+
include CafeCar::Model # Auto-included via engine
|
|
421
|
+
end
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The model gets:
|
|
425
|
+
|
|
426
|
+
- `sorted(*keys)` - Parse and apply sort parameters
|
|
427
|
+
- `normalize_sort_key(key)` - Internal helper that converts a sort key to Arel
|
|
428
|
+
order format
|
|
429
|
+
|
|
430
|
+
**Custom filters in controllers:**
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
class ProductsController < ApplicationController
|
|
434
|
+
cafe_car
|
|
435
|
+
|
|
436
|
+
private
|
|
437
|
+
|
|
438
|
+
def find_objects
|
|
439
|
+
@objects = model.where(active: true)
|
|
440
|
+
.query(filter_params)
|
|
441
|
+
.sorted(sort_params)
|
|
442
|
+
.page(page_params)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Advanced Usage
|
|
448
|
+
|
|
449
|
+
### Customizing Views
|
|
450
|
+
|
|
451
|
+
Override default views by creating templates in your application:
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
app/views/
|
|
455
|
+
products/
|
|
456
|
+
index.html.haml # Override index view
|
|
457
|
+
show.html.haml # Override show view
|
|
458
|
+
_form.html.haml # Override form partial
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
CafeCar's default views are in `app/views/cafe_car/application/` and serve as
|
|
462
|
+
templates.
|
|
463
|
+
|
|
464
|
+
### Custom Responders
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
class ProductsController < ApplicationController
|
|
468
|
+
cafe_car
|
|
469
|
+
|
|
470
|
+
private
|
|
471
|
+
|
|
472
|
+
def create
|
|
473
|
+
super
|
|
474
|
+
respond_with object, location: custom_path
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Authorization Helpers
|
|
480
|
+
|
|
481
|
+
In controllers:
|
|
482
|
+
|
|
483
|
+
```ruby
|
|
484
|
+
authorize! # Authorize current action
|
|
485
|
+
policy(object).update? # Check specific permission
|
|
486
|
+
policy(object).permitted_attributes # Get editable attributes
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
In views:
|
|
490
|
+
|
|
491
|
+
```erb
|
|
492
|
+
<% if policy(@product).update? %>
|
|
493
|
+
<%= link_to "Edit", edit_product_path(@product) %>
|
|
494
|
+
<% end %>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Current Context
|
|
498
|
+
|
|
499
|
+
Access current request context anywhere:
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
CafeCar::Current.user # Current user
|
|
503
|
+
CafeCar::Current.request_id # Request ID
|
|
504
|
+
CafeCar::Current.user_agent # User agent string
|
|
505
|
+
CafeCar::Current.ip_address # IP address
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Set in controllers via `set_current_attributes` (automatically called by
|
|
509
|
+
`cafe_car`).
|
|
510
|
+
|
|
511
|
+
## Sessions & Authentication
|
|
512
|
+
|
|
513
|
+
Sessions are **opt-in**. CafeCar works for plain CRUD with no login at all: when
|
|
514
|
+
a policy denies access and no sessions infrastructure is present, the request
|
|
515
|
+
gets a plain **403 Forbidden** instead of redirecting to a login page that
|
|
516
|
+
doesn't exist. Authorization (Pundit policies) is always on; *authentication*
|
|
517
|
+
(knowing who the user is) is the part you turn on when you want it.
|
|
518
|
+
|
|
519
|
+
### Enabling sessions
|
|
520
|
+
|
|
521
|
+
1. **Run the generator** to add the `sessions` table:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
$ rails generate cafe_car:sessions
|
|
525
|
+
$ rails db:migrate
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
The `CafeCar::Session` model and `SessionPolicy` ship with the engine, so the
|
|
529
|
+
generator only creates the migration (columns: `user`, `ip_address`,
|
|
530
|
+
`user_agent`).
|
|
531
|
+
|
|
532
|
+
2. **Expose the routes.** Mounting the engine already provides them. To expose
|
|
533
|
+
login at the top level without mounting, add to `config/routes.rb`:
|
|
534
|
+
|
|
535
|
+
```ruby
|
|
536
|
+
resource :session, only: %i[new create destroy], controller: "cafe_car/sessions"
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
This gives you `new_session_path` (login form) and `session_path` (create via
|
|
540
|
+
`POST`, log out via `DELETE`).
|
|
541
|
+
|
|
542
|
+
3. **Prepare your user model.** It needs `has_secure_password` and an `email`:
|
|
543
|
+
|
|
544
|
+
```ruby
|
|
545
|
+
class User < ApplicationRecord
|
|
546
|
+
has_secure_password
|
|
547
|
+
has_many :sessions, dependent: :destroy, class_name: "CafeCar::Session"
|
|
548
|
+
end
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
4. **Different user model name?** Set it in an initializer (resolved lazily):
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
# config/initializers/cafe_car.rb
|
|
555
|
+
CafeCar.user_class_name = "Account"
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Once sessions are available, an authorization failure for a signed-out visitor
|
|
559
|
+
redirects to the login form (remembering where they were headed) instead of
|
|
560
|
+
returning 403.
|
|
561
|
+
|
|
562
|
+
### Helpers
|
|
563
|
+
|
|
564
|
+
These are available in controllers and views:
|
|
565
|
+
|
|
566
|
+
- `authenticated?` - truthy when someone is logged in
|
|
567
|
+
- `current_user` - the logged-in user (or `nil`)
|
|
568
|
+
- `current_session` - the current `CafeCar::Session`
|
|
569
|
+
|
|
570
|
+
```erb
|
|
571
|
+
<% if authenticated? %>
|
|
572
|
+
Signed in as <%= current_user.email %>
|
|
573
|
+
<% else %>
|
|
574
|
+
<%= link_to "Log in", new_session_path %>
|
|
575
|
+
<% end %>
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Logging in (`POST /session` with `session[:email]`/`session[:password]`) sets a
|
|
579
|
+
signed, http-only cookie; logging out (`DELETE /session`) clears it.
|
|
580
|
+
|
|
581
|
+
## Generators
|
|
582
|
+
|
|
583
|
+
### Resource Generator
|
|
584
|
+
|
|
585
|
+
Generate a complete resource (model + controller + policy):
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
$ rails generate cafe_car:resource Product name:string price:decimal
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Controller Generator
|
|
592
|
+
|
|
593
|
+
Generate just a controller:
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
$ rails generate cafe_car:controller Products
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Policy Generator
|
|
600
|
+
|
|
601
|
+
Generate just a policy:
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
$ rails generate cafe_car:policy Product
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Notes Generator
|
|
608
|
+
|
|
609
|
+
Add polymorphic audit trail notes to your app:
|
|
610
|
+
|
|
15
611
|
```bash
|
|
16
|
-
$
|
|
612
|
+
$ rails generate cafe_car:notes
|
|
17
613
|
```
|
|
18
614
|
|
|
19
|
-
|
|
615
|
+
Creates:
|
|
616
|
+
|
|
617
|
+
- Migration for notes table
|
|
618
|
+
- `Note` model
|
|
619
|
+
- `Notable` concern for trackable models
|
|
620
|
+
|
|
621
|
+
### Sessions Generator
|
|
622
|
+
|
|
623
|
+
Enable opt-in login/logout (see [Sessions & Authentication](#sessions--authentication)):
|
|
624
|
+
|
|
20
625
|
```bash
|
|
21
|
-
$
|
|
626
|
+
$ rails generate cafe_car:sessions
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Creates the `sessions` table migration. The `CafeCar::Session` model and
|
|
630
|
+
`SessionPolicy` already ship with the engine.
|
|
631
|
+
|
|
632
|
+
## Configuration
|
|
633
|
+
|
|
634
|
+
### Custom Form Builder
|
|
635
|
+
|
|
636
|
+
```ruby
|
|
637
|
+
# config/initializers/cafe_car.rb
|
|
638
|
+
module CafeCar
|
|
639
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
|
640
|
+
# Your customizations
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Custom Presenter
|
|
646
|
+
|
|
647
|
+
```ruby
|
|
648
|
+
# app/presenters/application_presenter.rb
|
|
649
|
+
class ApplicationPresenter < CafeCar::Presenter
|
|
650
|
+
# Application-wide presenter customizations
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# app/presenters/product_presenter.rb
|
|
654
|
+
class ProductPresenter < ApplicationPresenter
|
|
655
|
+
show :name
|
|
656
|
+
show :price
|
|
657
|
+
end
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Custom Policy
|
|
661
|
+
|
|
662
|
+
```ruby
|
|
663
|
+
# app/policies/application_policy.rb
|
|
664
|
+
class ApplicationPolicy < CafeCar::ApplicationPolicy
|
|
665
|
+
def admin?
|
|
666
|
+
user&.admin?
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## Testing
|
|
672
|
+
|
|
673
|
+
CafeCar integrates with standard Rails testing tools:
|
|
674
|
+
|
|
675
|
+
```ruby
|
|
676
|
+
# test/controllers/products_controller_test.rb
|
|
677
|
+
class ProductsControllerTest < ActionDispatch::IntegrationTest
|
|
678
|
+
test "index displays products" do
|
|
679
|
+
get products_url
|
|
680
|
+
assert_response :success
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
test "create with valid attributes" do
|
|
684
|
+
assert_difference "Product.count", 1 do
|
|
685
|
+
post products_url, params: { product: { name: "Widget" } }
|
|
686
|
+
end
|
|
687
|
+
assert_redirected_to product_path(Product.last)
|
|
688
|
+
end
|
|
689
|
+
end
|
|
22
690
|
```
|
|
23
691
|
|
|
24
692
|
## Contributing
|
|
25
|
-
|
|
693
|
+
|
|
694
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for
|
|
695
|
+
development setup, how to run the tests (`bundle exec rake`), and PR expectations.
|
|
696
|
+
By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md). To report a
|
|
697
|
+
security issue, see [SECURITY.md](SECURITY.md).
|
|
26
698
|
|
|
27
699
|
## License
|
|
28
|
-
|
|
700
|
+
|
|
701
|
+
The gem is available as open source under the terms of the
|
|
702
|
+
[MIT License](https://opensource.org/licenses/MIT).
|