rider-kick 0.0.13 → 0.0.14
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 +629 -25
- data/lib/generators/rider_kick/USAGE +2 -0
- data/lib/generators/rider_kick/base_generator.rb +190 -0
- data/lib/generators/rider_kick/clean_arch_generator.rb +235 -45
- data/lib/generators/rider_kick/clean_arch_generator_engine_spec.rb +359 -0
- data/lib/generators/rider_kick/clean_arch_generator_factory_bot_spec.rb +131 -0
- data/lib/generators/rider_kick/entity_type_mapping_spec.rb +22 -13
- data/lib/generators/rider_kick/errors.rb +42 -0
- data/lib/generators/rider_kick/factory_generator.rb +238 -0
- data/lib/generators/rider_kick/factory_generator_spec.rb +175 -0
- data/lib/generators/rider_kick/repositories_contract_spec.rb +95 -22
- data/lib/generators/rider_kick/scaffold_generator.rb +377 -62
- data/lib/generators/rider_kick/scaffold_generator_builder_uploaders_spec.rb +119 -14
- data/lib/generators/rider_kick/scaffold_generator_conditional_filtering_spec.rb +820 -0
- data/lib/generators/rider_kick/scaffold_generator_contracts_spec.rb +37 -10
- data/lib/generators/rider_kick/scaffold_generator_contracts_with_scope_spec.rb +40 -11
- data/lib/generators/rider_kick/scaffold_generator_engine_spec.rb +221 -0
- data/lib/generators/rider_kick/scaffold_generator_idempotent_spec.rb +38 -13
- data/lib/generators/rider_kick/scaffold_generator_list_spec_format_spec.rb +153 -0
- data/lib/generators/rider_kick/scaffold_generator_rspec_spec.rb +347 -0
- data/lib/generators/rider_kick/scaffold_generator_success_spec.rb +31 -12
- data/lib/generators/rider_kick/scaffold_generator_with_scope_spec.rb +32 -11
- data/lib/generators/rider_kick/structure_generator.rb +154 -43
- data/lib/generators/rider_kick/structure_generator_comprehensive_spec.rb +598 -0
- data/lib/generators/rider_kick/structure_generator_engine_spec.rb +279 -0
- data/lib/generators/rider_kick/structure_generator_spec.rb +3 -3
- data/lib/generators/rider_kick/structure_generator_success_spec.rb +33 -5
- data/lib/generators/rider_kick/structure_generator_unit_spec.rb +2202 -0
- data/lib/generators/rider_kick/templates/.rubocop.yml +5 -4
- data/lib/generators/rider_kick/templates/config/initializers/version.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/db/migrate/20220613145533_init_database.rb +1 -1
- data/lib/generators/rider_kick/templates/db/structures/example.yaml.tt +140 -66
- data/lib/generators/rider_kick/templates/domains/core/builders/builder.rb.tt +36 -10
- data/lib/generators/rider_kick/templates/domains/core/builders/builder_spec.rb.tt +219 -0
- data/lib/generators/rider_kick/templates/domains/core/builders/error.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/builders/pagination.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/entities/entity.rb.tt +32 -14
- data/lib/generators/rider_kick/templates/domains/core/entities/error.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/domains/core/entities/pagination.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/domains/core/repositories/abstract_repository.rb.tt +4 -4
- data/lib/generators/rider_kick/templates/domains/core/repositories/create.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/repositories/create_spec.rb.tt +78 -0
- data/lib/generators/rider_kick/templates/domains/core/repositories/destroy.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/repositories/destroy_spec.rb.tt +88 -0
- data/lib/generators/rider_kick/templates/domains/core/repositories/fetch_by_id.rb.tt +3 -3
- data/lib/generators/rider_kick/templates/domains/core/repositories/fetch_by_id_spec.rb.tt +62 -0
- data/lib/generators/rider_kick/templates/domains/core/repositories/list.rb.tt +13 -8
- data/lib/generators/rider_kick/templates/domains/core/repositories/list_spec.rb.tt +190 -0
- data/lib/generators/rider_kick/templates/domains/core/repositories/update.rb.tt +4 -4
- data/lib/generators/rider_kick/templates/domains/core/repositories/update_spec.rb.tt +119 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/contract/default.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/domains/core/use_cases/contract/pagination.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/domains/core/use_cases/create.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/create_spec.rb.tt +71 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/destroy.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/destroy_spec.rb.tt +62 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/fetch_by_id.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/fetch_by_id_spec.rb.tt +62 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/get_version.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/use_cases/list.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/list_spec.rb.tt +64 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/update.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/update_spec.rb.tt +73 -0
- data/lib/generators/rider_kick/templates/domains/core/utils/abstract_utils.rb.tt +3 -3
- data/lib/generators/rider_kick/templates/domains/core/utils/request_methods.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/env.development +1 -1
- data/lib/generators/rider_kick/templates/env.production +1 -1
- data/lib/generators/rider_kick/templates/env.test +1 -1
- data/lib/generators/rider_kick/templates/models/{application_record.rb → application_record.rb.tt} +3 -1
- data/lib/generators/rider_kick/templates/models/model_spec.rb.tt +68 -0
- data/lib/generators/rider_kick/templates/spec/factories/.gitkeep +19 -0
- data/lib/generators/rider_kick/templates/spec/factories/factory.rb.tt +8 -0
- data/lib/generators/rider_kick/templates/spec/rails_helper.rb +2 -0
- data/lib/generators/rider_kick/templates/spec/support/class_stubber.rb +148 -0
- data/lib/generators/rider_kick/templates/spec/support/factory_bot.rb +34 -0
- data/lib/generators/rider_kick/templates/spec/support/faker.rb +61 -0
- data/lib/rider-kick.rb +8 -6
- data/lib/rider_kick/builders/abstract_active_record_entity_builder_spec.rb +644 -0
- data/lib/rider_kick/configuration.rb +238 -0
- data/lib/rider_kick/configuration_engine_spec.rb +377 -0
- data/lib/rider_kick/entities/failure_details.rb +1 -1
- data/lib/rider_kick/entities/failure_details_spec.rb +1 -1
- data/lib/rider_kick/matchers/use_case_result.rb +1 -1
- data/lib/rider_kick/use_cases/abstract_use_case.rb +1 -1
- data/lib/rider_kick/version.rb +1 -1
- metadata +129 -8
- data/CHANGELOG.md +0 -5
data/README.md
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
# RiderKick
|
|
2
2
|
Rails generators for **Clean Architecture** on the backend: use cases, entities, repositories, builders, and utilities — organized for clarity and designed for speed.
|
|
3
3
|
|
|
4
|
+
> **🎉 NEW!** Domain scoping dengan `--domain` option! Organize domain files ke dalam scope yang berbeda (core/, admin/, api/v1/, dll.)
|
|
5
|
+
>
|
|
6
|
+
> **🎉 NEW!** Sekarang dengan automatic RSpec generation! Setiap file yang di-generate otomatis mendapat spec file-nya. [Lihat dokumentasi lengkap](SPEC_GENERATION.md)
|
|
7
|
+
>
|
|
8
|
+
> **🎉 NEW!** FactoryBot factory generator dengan smart Faker generation! [Lihat dokumentasi lengkap](FACTORY_GENERATOR.md)
|
|
9
|
+
|
|
4
10
|
This gem provides helper interfaces and classes to assist in the construction of application with
|
|
5
11
|
Clean Architecture, as described in [Robert Martin's seminal book](https://www.amazon.com/gp/product/0134494164).
|
|
6
12
|
---
|
|
7
13
|
## Features
|
|
8
14
|
|
|
9
|
-
- **Clean Architecture scaffolding**
|
|
10
|
-
Creates `app/domains
|
|
11
|
-
- **
|
|
15
|
+
- **Clean Architecture scaffolding**
|
|
16
|
+
Creates `app/domains/<domain>/` with `entities/`, `use_cases/`, `repositories/`, `builders/`, and `utils/`.
|
|
17
|
+
- **Domain scoping dengan --domain option** 🆕
|
|
18
|
+
Organize domain files ke dalam scope yang berbeda: `--domain core/`, `--domain admin/`, `--domain api/v1/`, dll.
|
|
19
|
+
- **Engine support dengan --engine option**
|
|
20
|
+
Generate domain files dalam Rails engines: `--engine MyEngine --domain admin/`
|
|
21
|
+
- **Use-case-first "screaming architecture"**
|
|
12
22
|
Encourages file names like `[role]_[action]_[subject].rb` for immediate intent (e.g., `admin_update_stock.rb`).
|
|
13
|
-
- **Rails-native generators**
|
|
23
|
+
- **Rails-native generators**
|
|
14
24
|
Pragmatic commands for bootstrapping domain structure and scaffolding.
|
|
15
|
-
- **
|
|
25
|
+
- **Automatic RSpec generation** 🆕
|
|
26
|
+
Generates comprehensive RSpec files for all generated code (use cases, repositories, builders, entities).
|
|
27
|
+
- **FactoryBot factory generator** 🆕
|
|
28
|
+
Generates smart FactoryBot factories with automatic Faker values and foreign key skipping.
|
|
29
|
+
- **Idempotent, minimal friction**
|
|
16
30
|
Safe to run more than once; prefers appending or no-ops over destructive changes.
|
|
17
31
|
---
|
|
18
32
|
## ✅ Compatibility
|
|
@@ -30,9 +44,25 @@ And then execute:
|
|
|
30
44
|
$ bundle add rider-kick
|
|
31
45
|
$ rails generate rider_kick:clean_arch --setup
|
|
32
46
|
$ rails db:drop db:create db:migrate db:seed
|
|
33
|
-
$ rails g model models/products name price:decimal is_published:boolean
|
|
47
|
+
$ rails g model models/products name price:decimal is_published:boolean
|
|
34
48
|
$ rails generate rider_kick:structure Models::Product actor:owner
|
|
35
|
-
$ rails generate rider_kick:scaffold products scope:dashboard
|
|
49
|
+
$ rails generate rider_kick:scaffold products scope:dashboard
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Quick Examples with Domain Scoping
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Setup dengan domain default (core/)
|
|
56
|
+
$ rails generate rider_kick:clean_arch --setup
|
|
57
|
+
|
|
58
|
+
# Setup untuk admin domain
|
|
59
|
+
$ rails generate rider_kick:clean_arch --setup --domain admin/
|
|
60
|
+
|
|
61
|
+
# Setup untuk API v1 domain
|
|
62
|
+
$ rails generate rider_kick:clean_arch --setup --domain api/v1/
|
|
63
|
+
|
|
64
|
+
# Setup dalam Rails engine
|
|
65
|
+
$ rails generate rider_kick:clean_arch --setup --engine MyEngine --domain mobile/
|
|
36
66
|
```
|
|
37
67
|
### OPTIONAL
|
|
38
68
|
```bash
|
|
@@ -40,30 +70,558 @@ And then execute:
|
|
|
40
70
|
```
|
|
41
71
|
---
|
|
42
72
|
## Usage
|
|
73
|
+
|
|
74
|
+
### Initial Setup (Required Once)
|
|
75
|
+
|
|
76
|
+
#### Basic Setup (Default Domain)
|
|
77
|
+
```bash
|
|
78
|
+
# 1. Create new Rails app
|
|
79
|
+
rails new kotaro_minami -d=postgresql -T --skip-javascript --skip-asset-pipeline
|
|
80
|
+
|
|
81
|
+
# 2. Add rider-kick gem
|
|
82
|
+
bundle add rider-kick
|
|
83
|
+
|
|
84
|
+
# 3. Setup Clean Architecture structure (includes RSpec setup & helpers)
|
|
85
|
+
bin/rails generate rider_kick:clean_arch --setup
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Advanced Setup with Domain Scoping
|
|
89
|
+
|
|
43
90
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
# Setup untuk domain tertentu
|
|
92
|
+
bin/rails generate rider_kick:clean_arch --setup --domain admin/
|
|
93
|
+
|
|
94
|
+
# Setup untuk API domain
|
|
95
|
+
bin/rails generate rider_kick:clean_arch --setup --domain api/v1/
|
|
96
|
+
|
|
97
|
+
# Setup dalam Rails engine
|
|
98
|
+
bin/rails generate rider_kick:clean_arch --setup --engine MyEngine --domain core/
|
|
99
|
+
|
|
100
|
+
# Setup engine dengan domain khusus
|
|
101
|
+
bin/rails generate rider_kick:clean_arch --setup --engine AdminEngine --domain admin/
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Domain Scoping Explanation
|
|
105
|
+
|
|
106
|
+
**--domain option** memungkinkan Anda mengorganisir domain files ke dalam scope yang berbeda:
|
|
107
|
+
|
|
108
|
+
- **Default**: `--domain core/` → `app/domains/core/`
|
|
109
|
+
- **Admin domain**: `--domain admin/` → `app/domains/admin/`
|
|
110
|
+
- **API domain**: `--domain api/v1/` → `app/domains/api/v1/`
|
|
111
|
+
- **Engine**: `--engine MyEngine --domain mobile/` → `engines/my_engine/app/domains/mobile/`
|
|
112
|
+
|
|
113
|
+
### Setup Output
|
|
53
114
|
|
|
115
|
+
This setup will create:
|
|
116
|
+
- Domain structure (`app/domains/<domain>/` or `engines/<engine>/app/domains/<domain>/`)
|
|
117
|
+
- RSpec configuration with helpers (`spec/support/class_stubber.rb`, etc.)
|
|
118
|
+
- Database configuration
|
|
119
|
+
- Initializers
|
|
120
|
+
|
|
121
|
+
#### Generated Structure Examples
|
|
122
|
+
|
|
123
|
+
**Main App dengan domain default:**
|
|
124
|
+
```
|
|
125
|
+
app/
|
|
126
|
+
domains/
|
|
127
|
+
core/ # --domain core/ (default)
|
|
128
|
+
entities/
|
|
129
|
+
builders/
|
|
130
|
+
repositories/
|
|
131
|
+
use_cases/
|
|
132
|
+
utils/
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Main App dengan multiple domains:**
|
|
136
|
+
```
|
|
137
|
+
app/
|
|
138
|
+
domains/
|
|
139
|
+
core/ # --domain core/
|
|
140
|
+
admin/ # --domain admin/
|
|
141
|
+
api/
|
|
142
|
+
v1/ # --domain api/v1/
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Rails Engine:**
|
|
146
|
+
```
|
|
147
|
+
engines/
|
|
148
|
+
my_engine/
|
|
149
|
+
app/
|
|
150
|
+
domains/
|
|
151
|
+
core/ # --engine MyEngine --domain core/
|
|
152
|
+
mobile/ # --engine MyEngine --domain mobile/
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Generate Structure
|
|
156
|
+
|
|
157
|
+
Generator untuk membuat file struktur YAML dari model yang sudah ada. File YAML ini berisi konfigurasi yang akan digunakan oleh generator `scaffold`.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
rails generate rider_kick:structure MODEL_NAME [SETTINGS] [OPTIONS]
|
|
54
161
|
```
|
|
162
|
+
|
|
163
|
+
**Required Arguments:**
|
|
164
|
+
- `MODEL_NAME` - Nama model class (e.g., `Models::User`, `Models::Article`)
|
|
165
|
+
- `actor` - Actor/role yang akan menggunakan use case (e.g., `actor:user`, `actor:admin`)
|
|
166
|
+
- `resource_owner` - Nama resource owner untuk authorization (e.g., `resource_owner:account`)
|
|
167
|
+
- `resource_owner_id` - Nama kolom resource owner ID (e.g., `resource_owner_id:account_id`)
|
|
168
|
+
|
|
169
|
+
**Optional Settings:**
|
|
170
|
+
- `uploaders` - Daftar kolom uploader dipisah koma (e.g., `uploaders:avatar,images`)
|
|
171
|
+
- `search_able` - Daftar kolom yang bisa di-search dipisah koma (e.g., `search_able:name,email`)
|
|
172
|
+
|
|
173
|
+
**Options:**
|
|
174
|
+
- `--engine ENGINE_NAME` - Specify engine name (e.g., `Core`, `Admin`)
|
|
175
|
+
- `--domain DOMAIN` - Specify domain scope (e.g., `core/`, `admin/`, `api/v1/`)
|
|
176
|
+
|
|
177
|
+
**Examples:**
|
|
178
|
+
```bash
|
|
179
|
+
# Basic structure dengan domain default
|
|
180
|
+
rails generate rider_kick:structure Models::User actor:owner resource_owner:account resource_owner_id:account_id
|
|
181
|
+
|
|
182
|
+
# Dengan uploaders dan searchable fields
|
|
183
|
+
rails generate rider_kick:structure Models::Product \
|
|
184
|
+
actor:admin \
|
|
185
|
+
resource_owner:account \
|
|
186
|
+
resource_owner_id:account_id \
|
|
187
|
+
uploaders:image,documents \
|
|
188
|
+
search_able:name,sku \
|
|
189
|
+
--domain admin/
|
|
190
|
+
|
|
191
|
+
# Dalam Rails engine
|
|
192
|
+
rails generate rider_kick:structure Models::Order \
|
|
193
|
+
actor:user \
|
|
194
|
+
resource_owner:account \
|
|
195
|
+
resource_owner_id:account_id \
|
|
196
|
+
--engine OrderEngine \
|
|
197
|
+
--domain fulfillment/
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Output:** File YAML di `db/structures/<model_name>_structure.yaml` yang berisi konfigurasi lengkap untuk scaffold generator.
|
|
201
|
+
|
|
202
|
+
### Generate Scaffold
|
|
203
|
+
|
|
204
|
+
Generator utama untuk generate use cases, repositories, entities, builders, dan spec files berdasarkan structure YAML yang sudah dibuat.
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
rails generate rider_kick:scaffold STRUCTURE_NAME [SCOPE] [OPTIONS]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Required Arguments:**
|
|
211
|
+
- `STRUCTURE_NAME` - Nama structure (plural, tanpa `_structure.yaml`). Contoh: `users`, `products`, `orders`
|
|
212
|
+
|
|
213
|
+
**Optional Arguments:**
|
|
214
|
+
- `scope:SCOPE_NAME` - Route scope (e.g., `scope:dashboard`, `scope:admin`)
|
|
215
|
+
|
|
216
|
+
**Options:**
|
|
217
|
+
- `--engine ENGINE_NAME` - Specify engine name (e.g., `Core`, `Admin`)
|
|
218
|
+
- `--domain DOMAIN` - Specify domain scope (e.g., `core/`, `admin/`, `api/v1/`)
|
|
219
|
+
|
|
220
|
+
**Examples:**
|
|
221
|
+
```bash
|
|
222
|
+
# Basic scaffold dengan domain default
|
|
223
|
+
rails generate rider_kick:scaffold users scope:dashboard
|
|
224
|
+
|
|
225
|
+
# Dengan domain khusus
|
|
226
|
+
rails generate rider_kick:scaffold users scope:admin --domain admin/
|
|
227
|
+
|
|
228
|
+
# Dalam Rails engine
|
|
229
|
+
rails generate rider_kick:scaffold orders --engine OrderEngine --domain fulfillment/
|
|
230
|
+
|
|
231
|
+
# API domain
|
|
232
|
+
rails generate rider_kick:scaffold products --domain api/v1/
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Output:**
|
|
236
|
+
- Use cases: `app/domains/<domain>/use_cases/<scope>/<resource>/`
|
|
237
|
+
- Repositories: `app/domains/<domain>/repositories/<resource>/`
|
|
238
|
+
- Entities: `app/domains/<domain>/entities/`
|
|
239
|
+
- Builders: `app/domains/<domain>/builders/`
|
|
240
|
+
- Spec files untuk semua generated code
|
|
241
|
+
|
|
242
|
+
### Generate Factory
|
|
243
|
+
|
|
244
|
+
Generator untuk membuat FactoryBot factory files dengan smart Faker generation. Otomatis skip foreign key columns dan generate Faker values berdasarkan tipe kolom.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
rails generate rider_kick:factory MODEL_NAME [SCOPE] [OPTIONS]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Required Arguments:**
|
|
251
|
+
- `MODEL_NAME` - Nama model class (e.g., `Models::Article`, `Models::User`)
|
|
252
|
+
|
|
253
|
+
**Optional Arguments:**
|
|
254
|
+
- `scope:SCOPE_NAME` - Scope untuk factory (e.g., `scope:core`)
|
|
255
|
+
|
|
256
|
+
**Options:**
|
|
257
|
+
- `--engine ENGINE_NAME` - Specify engine name (e.g., `Core`, `Admin`)
|
|
258
|
+
- `--static` - Generate static values instead of Faker calls (time fields tetap menggunakan `Time.zone.now`)
|
|
259
|
+
|
|
260
|
+
**Examples:**
|
|
261
|
+
```bash
|
|
262
|
+
# Factory dengan Faker (default)
|
|
263
|
+
rails generate rider_kick:factory Models::Article scope:core
|
|
264
|
+
|
|
265
|
+
# Factory dengan static values
|
|
266
|
+
rails generate rider_kick:factory Models::Article scope:core --static
|
|
267
|
+
|
|
268
|
+
# Dalam Rails engine
|
|
269
|
+
rails generate rider_kick:factory Models::Order scope:fulfillment --engine OrderEngine
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Smart Faker Mapping:**
|
|
273
|
+
Generator menggunakan smart mapping berdasarkan nama kolom dan tipe:
|
|
274
|
+
- `string` dengan `email` → `Faker::Internet.email`
|
|
275
|
+
- `string` dengan `name` → `Faker::Name.name`
|
|
276
|
+
- `text` dengan `description`/`content` → `Faker::Lorem.paragraph`
|
|
277
|
+
- `integer` dengan `price`/`amount` → `Faker::Number.between(from: 1000, to: 1000000)`
|
|
278
|
+
- `decimal` dengan `price` → `Faker::Commerce.price`
|
|
279
|
+
- `boolean` → `[true, false].sample`
|
|
280
|
+
- `datetime`/`timestamp`/`time` → `Time.zone.now` (selalu)
|
|
281
|
+
- Dan banyak lagi...
|
|
282
|
+
|
|
283
|
+
**Kolom yang Di-skip:**
|
|
284
|
+
- `id`, `created_at`, `updated_at`, `type`
|
|
285
|
+
- Semua foreign key columns (`*_id`)
|
|
286
|
+
|
|
287
|
+
📖 **[Complete Factory Generator Documentation →](FACTORY_GENERATOR.md)**
|
|
288
|
+
📖 **[Domain Scoping Guide →](DOMAIN_SCOPING.md)**
|
|
55
289
|
---
|
|
56
290
|
## Generated Structure
|
|
57
291
|
|
|
292
|
+
### Default Structure (Main App)
|
|
293
|
+
```text
|
|
294
|
+
app/
|
|
295
|
+
domains/
|
|
296
|
+
core/ # Default domain (--domain core/)
|
|
297
|
+
entities/
|
|
298
|
+
builders/
|
|
299
|
+
repositories/
|
|
300
|
+
use_cases/
|
|
301
|
+
utils/
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Multiple Domains Structure
|
|
58
305
|
```text
|
|
59
306
|
app/
|
|
60
307
|
domains/
|
|
61
|
-
core/
|
|
308
|
+
core/ # Main domain (--domain core/)
|
|
309
|
+
entities/
|
|
310
|
+
builders/
|
|
311
|
+
repositories/
|
|
312
|
+
use_cases/
|
|
313
|
+
utils/
|
|
314
|
+
admin/ # Admin domain (--domain admin/)
|
|
62
315
|
entities/
|
|
63
316
|
builders/
|
|
64
317
|
repositories/
|
|
65
318
|
use_cases/
|
|
66
319
|
utils/
|
|
320
|
+
api/
|
|
321
|
+
v1/ # API domain (--domain api/v1/)
|
|
322
|
+
entities/
|
|
323
|
+
builders/
|
|
324
|
+
repositories/
|
|
325
|
+
use_cases/
|
|
326
|
+
utils/
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Rails Engine Structure
|
|
330
|
+
```text
|
|
331
|
+
engines/
|
|
332
|
+
my_engine/
|
|
333
|
+
app/
|
|
334
|
+
domains/
|
|
335
|
+
core/ # Engine domain (--engine MyEngine --domain core/)
|
|
336
|
+
entities/
|
|
337
|
+
builders/
|
|
338
|
+
repositories/
|
|
339
|
+
use_cases/
|
|
340
|
+
utils/
|
|
341
|
+
mobile/ # Mobile domain (--engine MyEngine --domain mobile/)
|
|
342
|
+
entities/
|
|
343
|
+
builders/
|
|
344
|
+
repositories/
|
|
345
|
+
use_cases/
|
|
346
|
+
utils/
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Complete Generator Documentation
|
|
352
|
+
|
|
353
|
+
### Generator Overview
|
|
354
|
+
|
|
355
|
+
Gem ini menyediakan **4 generator utama**:
|
|
356
|
+
|
|
357
|
+
1. **`rider_kick:clean_arch`** - Setup Clean Architecture structure
|
|
358
|
+
2. **`rider_kick:structure`** - Generate structure YAML file dari model
|
|
359
|
+
3. **`rider_kick:scaffold`** - Generate use cases, repositories, entities, builders
|
|
360
|
+
4. **`rider_kick:factory`** - Generate FactoryBot factory files
|
|
361
|
+
|
|
362
|
+
### 1. Generator: `rider_kick:clean_arch`
|
|
363
|
+
|
|
364
|
+
**Deskripsi:**
|
|
365
|
+
Generator untuk setup awal struktur Clean Architecture. Generator ini harus dijalankan pertama kali sebelum menggunakan generator lainnya.
|
|
366
|
+
|
|
367
|
+
**Command:**
|
|
368
|
+
```bash
|
|
369
|
+
rails generate rider_kick:clean_arch [OPTIONS]
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Options:**
|
|
373
|
+
|
|
374
|
+
| Option | Type | Default | Deskripsi |
|
|
375
|
+
|--------|------|---------|-----------|
|
|
376
|
+
| `--setup` | boolean | `false` | **WAJIB** - Setup domain structure. Harus dispecify untuk membuat struktur domain. |
|
|
377
|
+
| `--engine` | string | `nil` | Specify engine name (e.g., `Core`, `Admin`). Jika dispecify, `--setup` otomatis dianggap true. |
|
|
378
|
+
| `--domain` | string | `''` | Specify domain scope (e.g., `core/`, `admin/`, `api/v1/`). Default: `core/` |
|
|
379
|
+
|
|
380
|
+
**Yang Dihasilkan:**
|
|
381
|
+
|
|
382
|
+
1. **Domain Structure:**
|
|
383
|
+
- `app/domains/<domain>/use_cases/` (dengan subfolder `contract/`)
|
|
384
|
+
- `app/domains/<domain>/repositories/`
|
|
385
|
+
- `app/domains/<domain>/builders/`
|
|
386
|
+
- `app/domains/<domain>/entities/`
|
|
387
|
+
- `app/domains/<domain>/utils/`
|
|
388
|
+
|
|
389
|
+
2. **Base Files:**
|
|
390
|
+
- Contract files: `pagination.rb`, `default.rb`
|
|
391
|
+
- Use case: `get_version.rb`
|
|
392
|
+
- Builders: `error.rb`, `pagination.rb`
|
|
393
|
+
- Entities: `error.rb`, `pagination.rb`
|
|
394
|
+
- Repository: `abstract_repository.rb`
|
|
395
|
+
- Utils: `abstract_utils.rb`, `request_methods.rb`
|
|
396
|
+
|
|
397
|
+
3. **Configuration Files (Main App Only):**
|
|
398
|
+
- Initializers: `clean_archithecture.rb`, `generators.rb`, `hashie.rb`, `version.rb`, `zeitwerk.rb`, `pagy.rb`, `route_extensions.rb`
|
|
399
|
+
- Database config: `config/database.yml`
|
|
400
|
+
- Environment files: `.env.development`, `.env.production`, `.env.test`, `env.example`
|
|
401
|
+
- Git ignore: `.gitignore`
|
|
402
|
+
- Rubocop: `.rubocop.yml`
|
|
403
|
+
- README: `README.md`
|
|
404
|
+
|
|
405
|
+
4. **RSpec Setup (Main App Only):**
|
|
406
|
+
- RSpec configuration
|
|
407
|
+
- Support files: `class_stubber.rb`, `file_stuber.rb`, `repository_stubber.rb`
|
|
408
|
+
- FactoryBot & Faker setup
|
|
409
|
+
- `spec/rails_helper.rb`
|
|
410
|
+
|
|
411
|
+
5. **Database:**
|
|
412
|
+
- Migration: `db/migrate/20220613145533_init_database.rb`
|
|
413
|
+
- Structures directory: `db/structures/`
|
|
414
|
+
|
|
415
|
+
6. **Models:**
|
|
416
|
+
- `app/models/application_record.rb` (main app)
|
|
417
|
+
- `app/models/models/models.rb`
|
|
418
|
+
|
|
419
|
+
7. **Gem Dependencies (ditambahkan ke Gemfile):**
|
|
420
|
+
- `rspec-rails`, `factory_bot_rails`, `faker`, `shoulda-matchers`
|
|
421
|
+
- `dotenv-rails`
|
|
422
|
+
- `hashie`
|
|
423
|
+
- `image_processing`, `ruby-vips`
|
|
424
|
+
- `pagy`
|
|
425
|
+
|
|
426
|
+
**Catatan Penting:**
|
|
427
|
+
- Option `--setup` **WAJIB** dispecify untuk membuat struktur domain
|
|
428
|
+
- Jika `--engine` dispecify, `--setup` otomatis dianggap true
|
|
429
|
+
- Untuk engine, beberapa setup (seperti initializers) tidak dilakukan karena dilakukan di main app
|
|
430
|
+
|
|
431
|
+
### 2. Generator: `rider_kick:structure`
|
|
432
|
+
|
|
433
|
+
**Deskripsi:**
|
|
434
|
+
Generator untuk membuat file struktur YAML dari model yang sudah ada. File YAML ini berisi konfigurasi yang akan digunakan oleh generator `scaffold` untuk generate use cases, repositories, entities, dan builders.
|
|
435
|
+
|
|
436
|
+
**Command:**
|
|
437
|
+
```bash
|
|
438
|
+
rails generate rider_kick:structure MODEL_NAME [SETTINGS] [OPTIONS]
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Required Settings:**
|
|
442
|
+
|
|
443
|
+
| Setting | Deskripsi | Contoh |
|
|
444
|
+
|---------|-----------|--------|
|
|
445
|
+
| `actor` | Actor/role yang akan menggunakan use case | `actor:user`, `actor:admin`, `actor:owner` |
|
|
446
|
+
| `resource_owner` | Nama resource owner (untuk authorization) | `resource_owner:account` |
|
|
447
|
+
| `resource_owner_id` | Nama kolom resource owner ID | `resource_owner_id:account_id` |
|
|
448
|
+
|
|
449
|
+
**Optional Settings:**
|
|
450
|
+
|
|
451
|
+
| Setting | Deskripsi | Contoh |
|
|
452
|
+
|---------|-----------|--------|
|
|
453
|
+
| `uploaders` | Daftar kolom uploader (dipisah koma). Otomatis detect single/multiple berdasarkan singular/plural | `uploaders:avatar,images,picture` |
|
|
454
|
+
| `search_able` | Daftar kolom yang bisa di-search (dipisah koma) | `search_able:name,email,title` |
|
|
455
|
+
|
|
456
|
+
**Options:**
|
|
457
|
+
|
|
458
|
+
| Option | Type | Default | Deskripsi |
|
|
459
|
+
|--------|------|---------|-----------|
|
|
460
|
+
| `--engine` | string | `nil` | Specify engine name (e.g., `Core`, `Admin`) |
|
|
461
|
+
| `--domain` | string | `''` | Specify domain scope (e.g., `core/`, `admin/`, `api/v1/`) |
|
|
462
|
+
|
|
463
|
+
**Format YAML yang Dihasilkan:**
|
|
464
|
+
|
|
465
|
+
```yaml
|
|
466
|
+
model: Models::User
|
|
467
|
+
resource_name: users
|
|
468
|
+
actor: owner
|
|
469
|
+
resource_owner: account
|
|
470
|
+
resource_owner_id: account_id
|
|
471
|
+
uploaders:
|
|
472
|
+
- name: avatar
|
|
473
|
+
type: single
|
|
474
|
+
- name: images
|
|
475
|
+
type: multiple
|
|
476
|
+
search_able: []
|
|
477
|
+
domains:
|
|
478
|
+
action_list:
|
|
479
|
+
use_case:
|
|
480
|
+
contract: [...]
|
|
481
|
+
action_create:
|
|
482
|
+
use_case:
|
|
483
|
+
contract: [...]
|
|
484
|
+
action_update:
|
|
485
|
+
use_case:
|
|
486
|
+
contract: [...]
|
|
487
|
+
action_fetch_by_id:
|
|
488
|
+
use_case:
|
|
489
|
+
contract: [...]
|
|
490
|
+
action_destroy:
|
|
491
|
+
use_case:
|
|
492
|
+
contract: [...]
|
|
493
|
+
entity:
|
|
494
|
+
db_attributes: [...]
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Catatan Penting:**
|
|
498
|
+
- Model harus sudah ada sebelum menjalankan generator ini
|
|
499
|
+
- Generator ini membaca kolom dari model untuk generate contract dan entity attributes
|
|
500
|
+
- Kolom `id`, `created_at`, `updated_at`, `type` otomatis di-exclude dari contract fields
|
|
501
|
+
- Uploader type (single/multiple) otomatis di-detect berdasarkan singular/plural name
|
|
502
|
+
|
|
503
|
+
### 3. Generator: `rider_kick:scaffold`
|
|
504
|
+
|
|
505
|
+
**Deskripsi:**
|
|
506
|
+
Generator utama untuk generate use cases, repositories, entities, builders, dan spec files berdasarkan structure YAML yang sudah dibuat oleh generator `structure`.
|
|
507
|
+
|
|
508
|
+
**Command:**
|
|
509
|
+
```bash
|
|
510
|
+
rails generate rider_kick:scaffold STRUCTURE_NAME [SCOPE] [OPTIONS]
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Yang Dihasilkan:**
|
|
514
|
+
|
|
515
|
+
1. **Use Cases** (di `app/domains/<domain>/use_cases/<scope>/<resource>/`):
|
|
516
|
+
- `{actor}_create_{resource}.rb` - Create use case
|
|
517
|
+
- `{actor}_update_{resource}.rb` - Update use case
|
|
518
|
+
- `{actor}_list_{resource}.rb` - List use case
|
|
519
|
+
- `{actor}_fetch_by_id_{resource}.rb` - Fetch by ID use case
|
|
520
|
+
- `{actor}_destroy_{resource}.rb` - Destroy use case
|
|
521
|
+
- Spec files untuk setiap use case
|
|
522
|
+
|
|
523
|
+
2. **Repositories** (di `app/domains/<domain>/repositories/<resource>/`):
|
|
524
|
+
- `create_{resource}.rb` - Create repository
|
|
525
|
+
- `update_{resource}.rb` - Update repository
|
|
526
|
+
- `list_{resource}.rb` - List repository
|
|
527
|
+
- `fetch_by_id_{resource}.rb` - Fetch by ID repository
|
|
528
|
+
- `destroy_{resource}.rb` - Destroy repository
|
|
529
|
+
- Spec files untuk setiap repository
|
|
530
|
+
|
|
531
|
+
3. **Entities** (di `app/domains/<domain>/entities/`):
|
|
532
|
+
- `{resource}.rb` - Entity class dengan attributes dari model
|
|
533
|
+
|
|
534
|
+
4. **Builders** (di `app/domains/<domain>/builders/`):
|
|
535
|
+
- `{resource}.rb` - Builder class untuk convert ActiveRecord ke Entity
|
|
536
|
+
- Spec file untuk builder
|
|
537
|
+
|
|
538
|
+
5. **Model Spec** (di `app/models/models/` atau `app/models/<engine>/`):
|
|
539
|
+
- `{resource}_spec.rb` - Model spec file
|
|
540
|
+
|
|
541
|
+
6. **Model Attachment** (auto-inject ke model file):
|
|
542
|
+
- `has_one_attached` atau `has_many_attached` untuk uploaders
|
|
543
|
+
- Hanya jika model file sudah ada
|
|
544
|
+
|
|
545
|
+
**Catatan Penting:**
|
|
546
|
+
- Structure YAML file harus sudah ada (dibuat dengan generator `structure`)
|
|
547
|
+
- Generator ini membaca konfigurasi dari structure YAML file
|
|
548
|
+
- Validasi dilakukan untuk memastikan filter fields dan entity fields ada di model
|
|
549
|
+
- Uploader attachments otomatis di-inject ke model file jika file sudah ada
|
|
550
|
+
- Semua file yang di-generate otomatis mendapat spec file
|
|
551
|
+
|
|
552
|
+
### 4. Generator: `rider_kick:factory`
|
|
553
|
+
|
|
554
|
+
**Deskripsi:**
|
|
555
|
+
Generator untuk membuat FactoryBot factory files dengan smart Faker generation. Otomatis skip foreign key columns dan generate Faker values berdasarkan tipe kolom.
|
|
556
|
+
|
|
557
|
+
**Command:**
|
|
558
|
+
```bash
|
|
559
|
+
rails generate rider_kick:factory MODEL_NAME [SCOPE] [OPTIONS]
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Smart Faker Mapping:**
|
|
563
|
+
|
|
564
|
+
Generator ini menggunakan smart mapping berdasarkan nama kolom dan tipe:
|
|
565
|
+
|
|
566
|
+
| Tipe Kolom | Nama Kolom Contains | Faker Expression |
|
|
567
|
+
|------------|---------------------|-------------------|
|
|
568
|
+
| `string` | `email` | `Faker::Internet.email` |
|
|
569
|
+
| `string` | `name` | `Faker::Name.name` |
|
|
570
|
+
| `string` | `phone` | `Faker::PhoneNumber.phone_number` |
|
|
571
|
+
| `string` | `address` | `Faker::Address.full_address` |
|
|
572
|
+
| `string` | `title` | `Faker::Lorem.sentence(word_count: 3)` |
|
|
573
|
+
| `string` | `code` | `Faker::Alphanumeric.alphanumeric(number: 10)` |
|
|
574
|
+
| `string` | (default) | `Faker::Lorem.word` |
|
|
575
|
+
| `text` | `description`, `content`, `body` | `Faker::Lorem.paragraph(sentence_count: 3)` |
|
|
576
|
+
| `text` | (default) | `Faker::Lorem.sentence` |
|
|
577
|
+
| `integer` | `count`, `quantity` | `Faker::Number.between(from: 1, to: 100)` |
|
|
578
|
+
| `integer` | `age` | `Faker::Number.between(from: 18, to: 80)` |
|
|
579
|
+
| `integer` | `price`, `amount` | `Faker::Number.between(from: 1000, to: 1000000)` |
|
|
580
|
+
| `integer` | (default) | `Faker::Number.number(digits: 5)` |
|
|
581
|
+
| `decimal` | `price`, `amount` | `Faker::Commerce.price` |
|
|
582
|
+
| `decimal` | (default) | `Faker::Number.decimal(l_digits: 4, r_digits: 2)` |
|
|
583
|
+
| `boolean` | - | `[true, false].sample` |
|
|
584
|
+
| `date` | - | `Faker::Date.between(from: 1.year.ago, to: Date.today)` |
|
|
585
|
+
| `datetime`, `timestamp`, `time` | - | `Time.zone.now` (selalu, bahkan dengan `--static`) |
|
|
586
|
+
| `uuid` | - | `SecureRandom.uuid` |
|
|
587
|
+
| `json`, `jsonb` | - | `{ key: Faker::Lorem.word, value: Faker::Lorem.sentence }` |
|
|
588
|
+
|
|
589
|
+
**Kolom yang Di-skip:**
|
|
590
|
+
- `id` - Primary key
|
|
591
|
+
- `created_at` - Timestamp
|
|
592
|
+
- `updated_at` - Timestamp
|
|
593
|
+
- `type` - STI type
|
|
594
|
+
- `*_id` - Semua foreign key columns (ending with `_id`)
|
|
595
|
+
|
|
596
|
+
**Catatan Penting:**
|
|
597
|
+
- Model harus sudah ada sebelum menjalankan generator ini
|
|
598
|
+
- Foreign key columns otomatis di-skip
|
|
599
|
+
- Time-based columns selalu menggunakan `Time.zone.now`, bahkan dengan `--static`
|
|
600
|
+
- Dengan `--static`, Faker expressions akan di-evaluate dan hasilnya dijadikan static values
|
|
601
|
+
|
|
602
|
+
### Complete Workflow Example
|
|
603
|
+
|
|
604
|
+
```bash
|
|
605
|
+
# 1. Setup Clean Architecture (sekali di awal)
|
|
606
|
+
rails generate rider_kick:clean_arch --setup --domain core/
|
|
607
|
+
|
|
608
|
+
# 2. Buat model
|
|
609
|
+
rails g model models/products name:string price:decimal is_published:boolean
|
|
610
|
+
|
|
611
|
+
# 3. Generate structure YAML
|
|
612
|
+
rails generate rider_kick:structure Models::Product \
|
|
613
|
+
actor:admin \
|
|
614
|
+
resource_owner:account \
|
|
615
|
+
resource_owner_id:account_id \
|
|
616
|
+
uploaders:image \
|
|
617
|
+
search_able:name,description \
|
|
618
|
+
--domain core/
|
|
619
|
+
|
|
620
|
+
# 4. Generate scaffold (use cases, repositories, entities, builders)
|
|
621
|
+
rails generate rider_kick:scaffold products scope:dashboard --domain core/
|
|
622
|
+
|
|
623
|
+
# 5. Generate factory untuk testing
|
|
624
|
+
rails generate rider_kick:factory Models::Product scope:core
|
|
67
625
|
```
|
|
68
626
|
|
|
69
627
|
---
|
|
@@ -80,15 +638,37 @@ This structure provides helper interfaces and classes to assist in the construct
|
|
|
80
638
|
- models
|
|
81
639
|
- models
|
|
82
640
|
- ...
|
|
83
|
-
- domains
|
|
84
|
-
- core
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
641
|
+
- domains
|
|
642
|
+
- core # Default domain (--domain core/)
|
|
643
|
+
- entities (Contract Response)
|
|
644
|
+
- builders
|
|
645
|
+
- repositories (Business logic)
|
|
646
|
+
- use_cases (Just Usecase)
|
|
647
|
+
- utils (Class Reusable)
|
|
648
|
+
- admin # Admin domain (--domain admin/)
|
|
649
|
+
- entities
|
|
650
|
+
- builders
|
|
651
|
+
- repositories
|
|
652
|
+
- use_cases
|
|
653
|
+
- utils
|
|
654
|
+
- api/v1 # API domain (--domain api/v1/)
|
|
655
|
+
- entities
|
|
656
|
+
- builders
|
|
657
|
+
- repositories
|
|
658
|
+
- use_cases
|
|
659
|
+
- utils
|
|
91
660
|
```
|
|
661
|
+
|
|
662
|
+
### Domain Scoping untuk Large Applications
|
|
663
|
+
Untuk aplikasi yang besar, Anda dapat menggunakan `--domain` option untuk mengorganisir domain files berdasarkan konteks bisnis:
|
|
664
|
+
|
|
665
|
+
- **`core/`**: Domain utama aplikasi (default)
|
|
666
|
+
- **`admin/`**: Domain untuk fitur admin/pengelolaan
|
|
667
|
+
- **`api/v1/`**: Domain untuk API versioning
|
|
668
|
+
- **`mobile/`**: Domain untuk mobile-specific logic
|
|
669
|
+
- **`reporting/`**: Domain untuk laporan dan analytics
|
|
670
|
+
|
|
671
|
+
Ini membantu menjaga kode tetap terorganisir dan memudahkan maintenance seiring pertumbuhan aplikasi.
|
|
92
672
|
### Screaming architecture - use cases as an organisational principle
|
|
93
673
|
Uncle Bob suggests that your source code organisation should allow developers to easily find a listing of all use cases your application provides. Here's an example of how this might look in a this application.
|
|
94
674
|
```
|
|
@@ -117,6 +697,30 @@ Note that the use case name contains:
|
|
|
117
697
|
# fetch_info.rb [generic usecase] every role can access it
|
|
118
698
|
```
|
|
119
699
|
---
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## Dependencies
|
|
703
|
+
|
|
704
|
+
### Runtime Dependencies:
|
|
705
|
+
- `activesupport` >= 7.0, < 9.0
|
|
706
|
+
- `dry-matcher` >= 1.0, < 2.0
|
|
707
|
+
- `dry-monads` >= 1.6, < 2.0
|
|
708
|
+
- `dry-struct` >= 1.6, < 2.0
|
|
709
|
+
- `dry-types` >= 1.7, < 2.0
|
|
710
|
+
- `dry-validation` >= 1.9, < 2.0
|
|
711
|
+
- `hashie` >= 5.0, < 6.0
|
|
712
|
+
- `thor` >= 1.2, < 2.0
|
|
713
|
+
|
|
714
|
+
### Development Dependencies:
|
|
715
|
+
- `bundler` >= 2.4, < 3.0
|
|
716
|
+
- `generator_spec` >= 0.9, < 1.0
|
|
717
|
+
- `rake` >= 13.0, < 14.0
|
|
718
|
+
- `rspec` >= 3.12, < 4.0
|
|
719
|
+
- `rubocop` >= 1.63, < 2.0
|
|
720
|
+
- `rubocop-rspec` >= 3.0, < 4.0
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
120
724
|
## 🤝 Contributing
|
|
121
725
|
|
|
122
726
|
- Fork the repo & bundle install
|