data_porter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/blog-status.md +10 -0
  3. data/.claude/commands/blog.md +109 -0
  4. data/.claude/commands/task-done.md +27 -0
  5. data/.claude/commands/tm/add-dependency.md +58 -0
  6. data/.claude/commands/tm/add-subtask.md +79 -0
  7. data/.claude/commands/tm/add-task.md +81 -0
  8. data/.claude/commands/tm/analyze-complexity.md +124 -0
  9. data/.claude/commands/tm/analyze-project.md +100 -0
  10. data/.claude/commands/tm/auto-implement-tasks.md +100 -0
  11. data/.claude/commands/tm/command-pipeline.md +80 -0
  12. data/.claude/commands/tm/complexity-report.md +120 -0
  13. data/.claude/commands/tm/convert-task-to-subtask.md +74 -0
  14. data/.claude/commands/tm/expand-all-tasks.md +52 -0
  15. data/.claude/commands/tm/expand-task.md +52 -0
  16. data/.claude/commands/tm/fix-dependencies.md +82 -0
  17. data/.claude/commands/tm/help.md +101 -0
  18. data/.claude/commands/tm/init-project-quick.md +49 -0
  19. data/.claude/commands/tm/init-project.md +53 -0
  20. data/.claude/commands/tm/install-taskmaster.md +118 -0
  21. data/.claude/commands/tm/learn.md +106 -0
  22. data/.claude/commands/tm/list-tasks-by-status.md +42 -0
  23. data/.claude/commands/tm/list-tasks-with-subtasks.md +30 -0
  24. data/.claude/commands/tm/list-tasks.md +46 -0
  25. data/.claude/commands/tm/next-task.md +69 -0
  26. data/.claude/commands/tm/parse-prd-with-research.md +51 -0
  27. data/.claude/commands/tm/parse-prd.md +52 -0
  28. data/.claude/commands/tm/project-status.md +67 -0
  29. data/.claude/commands/tm/quick-install-taskmaster.md +23 -0
  30. data/.claude/commands/tm/remove-all-subtasks.md +94 -0
  31. data/.claude/commands/tm/remove-dependency.md +65 -0
  32. data/.claude/commands/tm/remove-subtask.md +87 -0
  33. data/.claude/commands/tm/remove-subtasks.md +89 -0
  34. data/.claude/commands/tm/remove-task.md +110 -0
  35. data/.claude/commands/tm/setup-models.md +52 -0
  36. data/.claude/commands/tm/show-task.md +85 -0
  37. data/.claude/commands/tm/smart-workflow.md +58 -0
  38. data/.claude/commands/tm/sync-readme.md +120 -0
  39. data/.claude/commands/tm/tm-main.md +147 -0
  40. data/.claude/commands/tm/to-cancelled.md +58 -0
  41. data/.claude/commands/tm/to-deferred.md +50 -0
  42. data/.claude/commands/tm/to-done.md +47 -0
  43. data/.claude/commands/tm/to-in-progress.md +39 -0
  44. data/.claude/commands/tm/to-pending.md +35 -0
  45. data/.claude/commands/tm/to-review.md +43 -0
  46. data/.claude/commands/tm/update-single-task.md +122 -0
  47. data/.claude/commands/tm/update-task.md +75 -0
  48. data/.claude/commands/tm/update-tasks-from-id.md +111 -0
  49. data/.claude/commands/tm/validate-dependencies.md +72 -0
  50. data/.claude/commands/tm/view-models.md +52 -0
  51. data/.env.example +12 -0
  52. data/.mcp.json +24 -0
  53. data/.taskmaster/CLAUDE.md +435 -0
  54. data/.taskmaster/config.json +44 -0
  55. data/.taskmaster/docs/prd.txt +2044 -0
  56. data/.taskmaster/state.json +6 -0
  57. data/.taskmaster/tasks/task_001.md +19 -0
  58. data/.taskmaster/tasks/task_002.md +19 -0
  59. data/.taskmaster/tasks/task_003.md +19 -0
  60. data/.taskmaster/tasks/task_004.md +19 -0
  61. data/.taskmaster/tasks/task_005.md +19 -0
  62. data/.taskmaster/tasks/task_006.md +19 -0
  63. data/.taskmaster/tasks/task_007.md +19 -0
  64. data/.taskmaster/tasks/task_008.md +19 -0
  65. data/.taskmaster/tasks/task_009.md +19 -0
  66. data/.taskmaster/tasks/task_010.md +19 -0
  67. data/.taskmaster/tasks/task_011.md +19 -0
  68. data/.taskmaster/tasks/task_012.md +19 -0
  69. data/.taskmaster/tasks/task_013.md +19 -0
  70. data/.taskmaster/tasks/task_014.md +19 -0
  71. data/.taskmaster/tasks/task_015.md +19 -0
  72. data/.taskmaster/tasks/task_016.md +19 -0
  73. data/.taskmaster/tasks/task_017.md +19 -0
  74. data/.taskmaster/tasks/task_018.md +19 -0
  75. data/.taskmaster/tasks/task_019.md +19 -0
  76. data/.taskmaster/tasks/task_020.md +19 -0
  77. data/.taskmaster/tasks/tasks.json +299 -0
  78. data/.taskmaster/templates/example_prd.txt +47 -0
  79. data/.taskmaster/templates/example_prd_rpg.txt +511 -0
  80. data/CHANGELOG.md +29 -0
  81. data/CLAUDE.md +65 -0
  82. data/CODE_OF_CONDUCT.md +10 -0
  83. data/CONTRIBUTING.md +49 -0
  84. data/LICENSE +21 -0
  85. data/README.md +463 -0
  86. data/Rakefile +12 -0
  87. data/app/assets/stylesheets/data_porter/application.css +646 -0
  88. data/app/channels/data_porter/import_channel.rb +10 -0
  89. data/app/controllers/data_porter/imports_controller.rb +68 -0
  90. data/app/javascript/data_porter/progress_controller.js +33 -0
  91. data/app/jobs/data_porter/dry_run_job.rb +12 -0
  92. data/app/jobs/data_porter/import_job.rb +12 -0
  93. data/app/jobs/data_porter/parse_job.rb +12 -0
  94. data/app/models/data_porter/data_import.rb +49 -0
  95. data/app/views/data_porter/imports/index.html.erb +142 -0
  96. data/app/views/data_porter/imports/new.html.erb +88 -0
  97. data/app/views/data_porter/imports/show.html.erb +49 -0
  98. data/config/database.yml +3 -0
  99. data/config/routes.rb +12 -0
  100. data/docs/SPEC.md +2012 -0
  101. data/docs/UI.md +32 -0
  102. data/docs/blog/001-why-build-a-data-import-engine.md +166 -0
  103. data/docs/blog/002-scaffolding-a-rails-engine.md +188 -0
  104. data/docs/blog/003-configuration-dsl.md +222 -0
  105. data/docs/blog/004-store-model-jsonb.md +237 -0
  106. data/docs/blog/005-target-dsl.md +284 -0
  107. data/docs/blog/006-parsing-csv-sources.md +300 -0
  108. data/docs/blog/007-orchestrator.md +247 -0
  109. data/docs/blog/008-actioncable-stimulus.md +376 -0
  110. data/docs/blog/009-phlex-ui-components.md +446 -0
  111. data/docs/blog/010-controllers-routing.md +374 -0
  112. data/docs/blog/011-generators.md +364 -0
  113. data/docs/blog/012-json-api-sources.md +323 -0
  114. data/docs/blog/013-testing-rails-engine.md +618 -0
  115. data/docs/blog/014-dry-run.md +307 -0
  116. data/docs/blog/015-publishing-retro.md +264 -0
  117. data/docs/blog/016-erb-view-templates.md +431 -0
  118. data/docs/blog/017-showcase-final-retro.md +220 -0
  119. data/docs/blog/BACKLOG.md +8 -0
  120. data/docs/blog/SERIES.md +154 -0
  121. data/docs/screenshots/index-with-previewing.jpg +0 -0
  122. data/docs/screenshots/index.jpg +0 -0
  123. data/docs/screenshots/modal-new-import.jpg +0 -0
  124. data/docs/screenshots/preview.jpg +0 -0
  125. data/lib/data_porter/broadcaster.rb +29 -0
  126. data/lib/data_porter/components/base.rb +10 -0
  127. data/lib/data_porter/components/failure_alert.rb +20 -0
  128. data/lib/data_porter/components/preview_table.rb +54 -0
  129. data/lib/data_porter/components/progress_bar.rb +33 -0
  130. data/lib/data_porter/components/results_summary.rb +19 -0
  131. data/lib/data_porter/components/status_badge.rb +16 -0
  132. data/lib/data_porter/components/summary_cards.rb +30 -0
  133. data/lib/data_porter/components.rb +14 -0
  134. data/lib/data_porter/configuration.rb +25 -0
  135. data/lib/data_porter/dsl/api_config.rb +25 -0
  136. data/lib/data_porter/dsl/column.rb +17 -0
  137. data/lib/data_porter/engine.rb +15 -0
  138. data/lib/data_porter/orchestrator.rb +141 -0
  139. data/lib/data_porter/record_validator.rb +32 -0
  140. data/lib/data_porter/registry.rb +33 -0
  141. data/lib/data_porter/sources/api.rb +49 -0
  142. data/lib/data_porter/sources/base.rb +35 -0
  143. data/lib/data_porter/sources/csv.rb +43 -0
  144. data/lib/data_porter/sources/json.rb +45 -0
  145. data/lib/data_porter/sources.rb +20 -0
  146. data/lib/data_porter/store_models/error.rb +13 -0
  147. data/lib/data_porter/store_models/import_record.rb +52 -0
  148. data/lib/data_porter/store_models/report.rb +21 -0
  149. data/lib/data_porter/target.rb +89 -0
  150. data/lib/data_porter/type_validator.rb +46 -0
  151. data/lib/data_porter/version.rb +5 -0
  152. data/lib/data_porter.rb +32 -0
  153. data/lib/generators/data_porter/install/install_generator.rb +33 -0
  154. data/lib/generators/data_porter/install/templates/create_data_porter_imports.rb.erb +21 -0
  155. data/lib/generators/data_porter/install/templates/initializer.rb +30 -0
  156. data/lib/generators/data_porter/target/target_generator.rb +44 -0
  157. data/lib/generators/data_porter/target/templates/target.rb.tt +20 -0
  158. data/sig/data_porter.rbs +4 -0
  159. metadata +274 -0
data/README.md ADDED
@@ -0,0 +1,463 @@
1
+ # DataPorter
2
+
3
+ A mountable Rails engine for 3-step data import workflows: **Upload**, **Preview**, **Import**.
4
+
5
+ Supports CSV, JSON, and API sources with a declarative DSL for defining import targets. Business-agnostic by design -- all domain logic lives in your host app.
6
+
7
+ ![Import list with status badges](docs/screenshots/index-with-previewing.jpg)
8
+
9
+ ![New import modal with dropzone](docs/screenshots/modal-new-import.jpg)
10
+
11
+ ![Preview with summary cards and data table](docs/screenshots/preview.jpg)
12
+
13
+ ## Requirements
14
+
15
+ - Ruby >= 3.2
16
+ - Rails >= 7.0
17
+ - ActionCable (for real-time progress updates)
18
+ - ActiveStorage (for file uploads)
19
+
20
+ ## Installation
21
+
22
+ Add the gem to your Gemfile:
23
+
24
+ ```bash
25
+ bundle add data_porter
26
+ ```
27
+
28
+ Run the install generator:
29
+
30
+ ```bash
31
+ bin/rails generate data_porter:install
32
+ ```
33
+
34
+ This will:
35
+ - Create the migration for `data_porter_imports`
36
+ - Add an initializer at `config/initializers/data_porter.rb`
37
+ - Create the `app/importers/` directory
38
+ - Mount the engine at `/imports`
39
+
40
+ Run the migration:
41
+
42
+ ```bash
43
+ bin/rails db:migrate
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ Generate a target:
49
+
50
+ ```bash
51
+ bin/rails generate data_porter:target Product name:string:required price:integer sku:string
52
+ ```
53
+
54
+ Implement the `persist` method in `app/importers/product_target.rb`:
55
+
56
+ ```ruby
57
+ # frozen_string_literal: true
58
+
59
+ class ProductTarget < DataPorter::Target
60
+ label "Product"
61
+ model_name "Product"
62
+ icon "fas fa-file-import"
63
+ sources :csv
64
+
65
+ columns do
66
+ column :name, type: :string, required: true
67
+ column :price, type: :integer
68
+ column :sku, type: :string
69
+ end
70
+
71
+ def persist(record, context:)
72
+ Product.create!(record.attributes)
73
+ end
74
+ end
75
+ ```
76
+
77
+ Visit `/imports` and start importing.
78
+
79
+ ## Configuration
80
+
81
+ All options are set in `config/initializers/data_porter.rb`:
82
+
83
+ ```ruby
84
+ DataPorter.configure do |config|
85
+ # Parent controller for the engine's controllers to inherit from.
86
+ # Controls authentication, layouts, and helpers.
87
+ config.parent_controller = "ApplicationController"
88
+
89
+ # ActiveJob queue name for import jobs.
90
+ config.queue_name = :imports
91
+
92
+ # ActiveStorage service for uploaded files.
93
+ config.storage_service = :local
94
+
95
+ # ActionCable channel prefix.
96
+ config.cable_channel_prefix = "data_porter"
97
+
98
+ # Context builder: inject business data into targets.
99
+ # Receives the current controller instance.
100
+ config.context_builder = ->(controller) {
101
+ OpenStruct.new(user: controller.current_user)
102
+ }
103
+
104
+ # Maximum number of records displayed in preview.
105
+ config.preview_limit = 500
106
+
107
+ # Enabled source types.
108
+ config.enabled_sources = %i[csv json api]
109
+ end
110
+ ```
111
+
112
+ | Option | Default | Description |
113
+ |---|---|---|
114
+ | `parent_controller` | `"ApplicationController"` | Controller class the engine inherits from |
115
+ | `queue_name` | `:imports` | ActiveJob queue for import jobs |
116
+ | `storage_service` | `:local` | ActiveStorage service name |
117
+ | `cable_channel_prefix` | `"data_porter"` | ActionCable stream prefix |
118
+ | `context_builder` | `nil` | Lambda receiving the controller, returns context passed to target methods |
119
+ | `preview_limit` | `500` | Max records shown in the preview step |
120
+ | `enabled_sources` | `%i[csv json api]` | Source types available in the UI |
121
+
122
+ ## Defining Targets
123
+
124
+ Targets are plain Ruby classes in `app/importers/` that inherit from `DataPorter::Target`.
125
+
126
+ ### Class-level DSL
127
+
128
+ ```ruby
129
+ class OrderTarget < DataPorter::Target
130
+ label "Orders"
131
+ model_name "Order"
132
+ icon "fas fa-shopping-cart"
133
+ sources :csv, :json, :api
134
+
135
+ columns do
136
+ column :order_number, type: :string, required: true
137
+ column :total, type: :decimal
138
+ column :placed_at, type: :date
139
+ column :active, type: :boolean
140
+ column :quantity, type: :integer
141
+ end
142
+
143
+ csv_mapping do
144
+ map "Order #" => :order_number
145
+ map "Total ($)" => :total
146
+ end
147
+
148
+ json_root "data.orders"
149
+
150
+ api_config do
151
+ endpoint "https://api.example.com/orders"
152
+ headers({ "Authorization" => "Bearer token" })
153
+ response_root "data.orders"
154
+ end
155
+
156
+ deduplicate_by :order_number
157
+
158
+ dry_run_enabled
159
+
160
+ # ...
161
+ end
162
+ ```
163
+
164
+ #### `label(value)`
165
+
166
+ Human-readable name shown in the UI.
167
+
168
+ #### `model_name(value)`
169
+
170
+ The ActiveRecord model name this target imports into (for display purposes).
171
+
172
+ #### `icon(value)`
173
+
174
+ CSS icon class (e.g. FontAwesome) shown in the UI.
175
+
176
+ #### `sources(*types)`
177
+
178
+ Accepted source types: `:csv`, `:json`, `:api`.
179
+
180
+ #### `columns { ... }`
181
+
182
+ Defines the expected columns for this import. Each column accepts:
183
+
184
+ | Parameter | Type | Default | Description |
185
+ |---|---|---|---|
186
+ | `name` | Symbol | (required) | Column identifier |
187
+ | `type` | Symbol | `:string` | One of `:string`, `:integer`, `:decimal`, `:boolean`, `:date` |
188
+ | `required` | Boolean | `false` | Whether the column must have a value |
189
+ | `label` | String | Humanized name | Display label in the preview |
190
+
191
+ #### `csv_mapping { ... }`
192
+
193
+ Maps CSV header names to column names when they don't match:
194
+
195
+ ```ruby
196
+ csv_mapping do
197
+ map "First Name" => :first_name
198
+ map "E-mail" => :email
199
+ end
200
+ ```
201
+
202
+ #### `json_root(path)`
203
+
204
+ Dot-separated path to the array of records within a JSON document:
205
+
206
+ ```ruby
207
+ json_root "data.users"
208
+ ```
209
+
210
+ Given `{ "data": { "users": [...] } }`, records are extracted from `data.users`.
211
+
212
+ #### `api_config { ... }`
213
+
214
+ Configures the API source:
215
+
216
+ ```ruby
217
+ api_config do
218
+ endpoint "https://api.example.com/records"
219
+ headers({ "Authorization" => "Bearer token", "Accept" => "application/json" })
220
+ response_root "data.items"
221
+ end
222
+ ```
223
+
224
+ #### `deduplicate_by(*keys)`
225
+
226
+ Skip records that share the same value(s) for the given column(s):
227
+
228
+ ```ruby
229
+ deduplicate_by :email
230
+ deduplicate_by :first_name, :last_name
231
+ ```
232
+
233
+ #### `dry_run_enabled`
234
+
235
+ Enables dry run mode for this target (see [Dry Run](#dry-run)).
236
+
237
+ ### Instance Methods
238
+
239
+ Override these in your target to customize behavior:
240
+
241
+ #### `transform(record)`
242
+
243
+ Transform a record before validation. Must return the (modified) record.
244
+
245
+ ```ruby
246
+ def transform(record)
247
+ record.attributes["email"] = record.attributes["email"]&.downcase
248
+ record
249
+ end
250
+ ```
251
+
252
+ #### `validate(record)`
253
+
254
+ Add custom validation errors to a record:
255
+
256
+ ```ruby
257
+ def validate(record)
258
+ record.add_error("Email is invalid") unless record.attributes["email"]&.include?("@")
259
+ end
260
+ ```
261
+
262
+ #### `persist(record, context:)`
263
+
264
+ **Required.** Save the record to your database. Raises `NotImplementedError` if not overridden.
265
+
266
+ ```ruby
267
+ def persist(record, context:)
268
+ User.create!(record.attributes)
269
+ end
270
+ ```
271
+
272
+ #### `after_import(results, context:)`
273
+
274
+ Called once after all records have been processed:
275
+
276
+ ```ruby
277
+ def after_import(results, context:)
278
+ AdminMailer.import_complete(context.user, results).deliver_later
279
+ end
280
+ ```
281
+
282
+ #### `on_error(record, error, context:)`
283
+
284
+ Called when a record fails to import:
285
+
286
+ ```ruby
287
+ def on_error(record, error, context:)
288
+ Sentry.capture_exception(error, extra: { record: record.attributes })
289
+ end
290
+ ```
291
+
292
+ ## Source Types
293
+
294
+ ### CSV
295
+
296
+ Upload a CSV file. Configure header mappings with `csv_mapping` when headers don't match your column names.
297
+
298
+ ### JSON
299
+
300
+ Upload a JSON file. Use `json_root` to specify the path to the records array. Raw JSON arrays are supported without `json_root`.
301
+
302
+ ### API
303
+
304
+ Fetch records from an external API endpoint. No file upload is needed -- the engine calls the API directly.
305
+
306
+ #### Basic usage
307
+
308
+ ```ruby
309
+ api_config do
310
+ endpoint "https://api.example.com/data"
311
+ headers({ "Authorization" => "Bearer token" })
312
+ response_root "results"
313
+ end
314
+ ```
315
+
316
+ | Option | Type | Description |
317
+ |---|---|---|
318
+ | `endpoint` | String or Proc | URL to fetch records from |
319
+ | `headers` | Hash or Proc | HTTP headers sent with the request |
320
+ | `response_root` | String | Key in the JSON response containing the records array (omit for top-level arrays) |
321
+
322
+ #### Dynamic endpoints and headers
323
+
324
+ Both `endpoint` and `headers` accept lambdas for runtime values. The endpoint lambda receives the import's `config` hash (populated from the form):
325
+
326
+ ```ruby
327
+ api_config do
328
+ endpoint ->(params) { "https://api.example.com/events?page=#{params[:page]}" }
329
+ headers -> { { "Authorization" => "Bearer #{ENV['API_TOKEN']}" } }
330
+ response_root "data"
331
+ end
332
+ ```
333
+
334
+ #### Example: importing from a paginated API
335
+
336
+ ```ruby
337
+ class EventTarget < DataPorter::Target
338
+ label "Events"
339
+ model_name "Event"
340
+ sources :api
341
+
342
+ api_config do
343
+ endpoint "https://api.example.com/events"
344
+ headers -> { { "Authorization" => "Bearer #{ENV['EVENTS_API_KEY']}", "Accept" => "application/json" } }
345
+ response_root "events"
346
+ end
347
+
348
+ columns do
349
+ column :name, type: :string, required: true
350
+ column :date, type: :date
351
+ column :venue, type: :string
352
+ column :capacity, type: :integer
353
+ end
354
+
355
+ def persist(record, context:)
356
+ Event.create!(record.attributes)
357
+ end
358
+ end
359
+ ```
360
+
361
+ When a user creates an import with source type **API**, the engine skips file upload entirely, calls the configured endpoint, parses the JSON response, and feeds the records through the same preview/validate/import pipeline as CSV and JSON sources.
362
+
363
+ ## Import Workflow
364
+
365
+ Each import progresses through these statuses:
366
+
367
+ ```
368
+ pending -> parsing -> previewing -> importing -> completed
369
+ \-> failed
370
+ pending -> parsing -> dry_running -> previewing
371
+ ```
372
+
373
+ | Status | Description |
374
+ |---|---|
375
+ | `pending` | Import created, waiting for file/source |
376
+ | `parsing` | Source is being read and records extracted |
377
+ | `previewing` | Records parsed and ready for review |
378
+ | `importing` | Records are being persisted |
379
+ | `completed` | All records processed |
380
+ | `failed` | Import encountered a fatal error |
381
+ | `dry_running` | Dry run validation in progress |
382
+
383
+ ### Routes
384
+
385
+ The engine provides these routes (mounted at your chosen path):
386
+
387
+ | Method | Path | Action |
388
+ |---|---|---|
389
+ | GET | `/imports` | List imports |
390
+ | GET | `/imports/new` | New import form |
391
+ | POST | `/imports` | Create import |
392
+ | GET | `/imports/:id` | Show import |
393
+ | POST | `/imports/:id/parse` | Parse uploaded source |
394
+ | POST | `/imports/:id/confirm` | Confirm and run import |
395
+ | POST | `/imports/:id/cancel` | Cancel import |
396
+ | POST | `/imports/:id/dry_run` | Run dry validation |
397
+
398
+ ## Dry Run
399
+
400
+ When `dry_run_enabled` is declared on a target, a "Dry Run" button appears in the preview step. Dry run executes the full import pipeline (transform, validate, persist) inside a rolled-back transaction, giving you a validation report without modifying the database.
401
+
402
+ ## Real-time Updates
403
+
404
+ DataPorter broadcasts import progress via ActionCable. The channel streams on:
405
+
406
+ ```
407
+ #{cable_channel_prefix}/imports/#{import_id}
408
+ ```
409
+
410
+ The default prefix is `data_porter`, so a typical stream name is `data_porter/imports/42`.
411
+
412
+ The engine ships with a Stimulus controller that automatically subscribes to the channel and updates the UI during parsing and importing.
413
+
414
+ ## Generators
415
+
416
+ ### `data_porter:install`
417
+
418
+ ```bash
419
+ bin/rails generate data_porter:install
420
+ ```
421
+
422
+ Sets up the migration, initializer, `app/importers/` directory, and mounts the engine.
423
+
424
+ ### `data_porter:target`
425
+
426
+ ```bash
427
+ bin/rails generate data_porter:target ModelName column:type[:required] ...
428
+ ```
429
+
430
+ Examples:
431
+
432
+ ```bash
433
+ bin/rails generate data_porter:target User email:string:required name:string age:integer
434
+ bin/rails generate data_porter:target Product name:string price:decimal
435
+ ```
436
+
437
+ Column format: `name:type[:required]`
438
+
439
+ Supported types: `string`, `integer`, `decimal`, `boolean`, `date`.
440
+
441
+ ## Development
442
+
443
+ ```bash
444
+ git clone https://github.com/SerylLns/data_porter.git
445
+ cd data_porter
446
+ bin/setup
447
+ ```
448
+
449
+ Run the test suite:
450
+
451
+ ```bash
452
+ bundle exec rspec
453
+ ```
454
+
455
+ Run the linter:
456
+
457
+ ```bash
458
+ bundle exec rubocop
459
+ ```
460
+
461
+ ## License
462
+
463
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]