dami 1.0.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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +306 -0
  3. data/LICENSE +21 -0
  4. data/README.md +106 -0
  5. data/bin/dami +10 -0
  6. data/docs/01.Getting_Started.md +220 -0
  7. data/docs/02.Models_and_Fields.md +244 -0
  8. data/docs/03.Querying.md +387 -0
  9. data/docs/04.Creating_Updating_Deleting.md +226 -0
  10. data/docs/05.Validation.md +259 -0
  11. data/docs/06.Protection.md +160 -0
  12. data/docs/07.Associations.md +239 -0
  13. data/docs/08.Scopes.md +99 -0
  14. data/docs/09.Migrations.md +165 -0
  15. data/docs/10.Flows_and_Commands.md +181 -0
  16. data/docs/11.Localization.md +435 -0
  17. data/docs/12.TheDamiWay.md +227 -0
  18. data/docs/Manifesto.md +305 -0
  19. data/docs/site.md +477 -0
  20. data/lib/dami/actions/command.rb +80 -0
  21. data/lib/dami/actions/context.rb +63 -0
  22. data/lib/dami/actions/draft.rb +36 -0
  23. data/lib/dami/actions/flow.rb +42 -0
  24. data/lib/dami/adapters/base.rb +40 -0
  25. data/lib/dami/adapters/sqlite/connection.rb +122 -0
  26. data/lib/dami/adapters/sqlite/core.rb +17 -0
  27. data/lib/dami/adapters/sqlite/query.rb +353 -0
  28. data/lib/dami/adapters/sqlite/schema.rb +135 -0
  29. data/lib/dami/cli.rb +117 -0
  30. data/lib/dami/configuration.rb +246 -0
  31. data/lib/dami/core.rb +98 -0
  32. data/lib/dami/dsl_guardrails.rb +45 -0
  33. data/lib/dami/errors.rb +89 -0
  34. data/lib/dami/inflector.rb +245 -0
  35. data/lib/dami/localization.rb +131 -0
  36. data/lib/dami/migration.rb +84 -0
  37. data/lib/dami/migrator.rb +105 -0
  38. data/lib/dami/plugins/associations.rb +284 -0
  39. data/lib/dami/plugins/nested_attributes.rb +180 -0
  40. data/lib/dami/plugins/protection.rb +30 -0
  41. data/lib/dami/plugins/validations.rb +90 -0
  42. data/lib/dami/query/builder.rb +132 -0
  43. data/lib/dami/query/enumerable.rb +62 -0
  44. data/lib/dami/query/persistence.rb +133 -0
  45. data/lib/dami/record_proxy.rb +32 -0
  46. data/lib/dami/result.rb +27 -0
  47. data/lib/dami/schema/diff.rb +84 -0
  48. data/lib/dami/schema/dumper.rb +60 -0
  49. data/lib/dami/schema/generator.rb +69 -0
  50. data/lib/dami/schema/introspector.rb +34 -0
  51. data/lib/dami/schema/loader.rb +58 -0
  52. data/lib/dami/schema.rb +13 -0
  53. data/lib/dami/validation_rules.rb +72 -0
  54. data/lib/dami/version.rb +3 -0
  55. data/lib/dami.rb +32 -0
  56. metadata +218 -0
data/docs/site.md ADDED
@@ -0,0 +1,477 @@
1
+ # **The Dami Manifesto: Architecture You Can't Break**
2
+
3
+ ## The Problem Every Rails Developer Knows
4
+
5
+ Open any Rails codebase that's been in production for more than a year. You'll find the same problems:
6
+
7
+ **The 2,000-line `User` model** that handles everything from database schema to email formatting to birthday notifications. It started clean. It decayed slowly. Now nobody wants to touch it.
8
+
9
+ **The callback chain of death.** A simple `user.save` triggers 47 invisible operations. When one fails, you spend days debugging why users got created but welcome emails didn't send.
10
+
11
+ **The copy-paste validation epidemic.** That phone number regex exists in 30 models. You need to update it. Good luck.
12
+
13
+ **The scattered YAML translation files.** Validation messages in one file, model names in another, error messages somewhere else. Keeping them synchronized is a nightmare.
14
+
15
+ These aren't failures of discipline. **They're failures of tooling.**
16
+
17
+ Rails doesn't prevent bad architecture—it just makes it easy to write. The result is technical debt that compounds daily until your codebase becomes unmaintainable.
18
+
19
+ **Dami solves this. Permanently.**
20
+
21
+ ---
22
+
23
+ ## The Solution: Six Architectural Guarantees
24
+
25
+ Dami isn't trying to be everything to everyone. It's a precision tool that solves the hardest problems in application architecture.
26
+
27
+ ### **1. Enforced Separation: Fat Models Are Impossible**
28
+
29
+ Most frameworks suggest good architecture. Dami **enforces** it.
30
+
31
+ The Four Pillars separate concerns at the DSL level:
32
+ - `Dami.model` - Structure only
33
+ - `Dami.behavior` - Write-side rules only
34
+ - `Dami.scopes` - Query logic only
35
+ - `Dami.flow` - Business workflows
36
+
37
+ Try to put a validation in your model? **The framework stops you:**
38
+
39
+ ```ruby
40
+ Dami.model :users do
41
+ validate { rule :email, :required }
42
+ end
43
+
44
+ # => Dami::InvalidDSLError: 'validate' not allowed here.
45
+ # Define it in a Dami.behavior block.
46
+ ```
47
+
48
+ **This isn't a suggestion. It's a compile-time guarantee.** Your architecture can't decay because the framework won't let it.
49
+
50
+ ---
51
+
52
+ ### **2. Validation as Infrastructure**
53
+
54
+ Stop copy-pasting validation logic across models. Dami treats validations as reusable components:
55
+
56
+ ```ruby
57
+ # Define once
58
+ Dami.rules :default, {
59
+ phone: { check: /\A\d{3}-\d{3}-\d{4}\z/, message: "Invalid format" }
60
+ }
61
+
62
+ # Use everywhere
63
+ Dami.behavior(:users) { validate { rule :phone, :phone } }
64
+ Dami.behavior(:contacts) { validate { rule :mobile, :phone } }
65
+ Dami.behavior(:vendors) { validate { rule :support_line, :phone } }
66
+ ```
67
+
68
+ **Update once. Updates everywhere.** Perfect consistency across your entire application.
69
+
70
+ ---
71
+
72
+ ### **3. I18n as Core Infrastructure**
73
+
74
+ Rails bolts on internationalization as an afterthought. Dami builds it in as **first-class infrastructure.**
75
+
76
+ One DSL for all user-facing text:
77
+
78
+ ```ruby
79
+ Dami.localize :validations do
80
+ en { set :required, "cannot be blank" }
81
+ es { set :required, "no puede estar en blanco" }
82
+ fr { set :required, "ne peut pas être vide" }
83
+ end
84
+ ```
85
+
86
+ **Auto-detection included.** Using Rails or Sinatra with the i18n gem? Dami automatically uses `I18n.locale`. Zero configuration.
87
+
88
+ Four translation scopes cover everything:
89
+ - `:validations` - Error messages
90
+ - `:models` - Attribute names for forms
91
+ - `:enums` - Display values
92
+ - `:errors` - System exceptions
93
+
94
+ **One source of truth. Any language. Zero overhead.**
95
+
96
+ ---
97
+
98
+ ### **4. Migrations From Intent**
99
+
100
+ Stop writing migration files. Describe what you want. Dami generates the how.
101
+
102
+ **Define the model:**
103
+ ```ruby
104
+ Dami.model :users do
105
+ fields { field :name, :string }
106
+ end
107
+ ```
108
+
109
+ **Generate the migration:**
110
+ ```bash
111
+ $ dami generate migration CreateUsers
112
+ ✅ Migration created: db/migrations/20251018_create_users.rb
113
+ ```
114
+
115
+ **Add a field:**
116
+ ```ruby
117
+ Dami.model :users do
118
+ fields do
119
+ field :name, :string
120
+ field :status, :string # New
121
+ end
122
+ end
123
+ ```
124
+
125
+ **Generate again:**
126
+ ```bash
127
+ $ dami generate migration AddStatusToUsers
128
+ ✅ Migration created!
129
+ ```
130
+
131
+ Dami compares model to schema. Writes perfect, reversible migrations. **The tedium is eliminated.**
132
+
133
+ ---
134
+
135
+ ### **5. Transactional Workflows**
136
+
137
+ Rails callbacks scatter logic across time and space. Dami makes business logic **explicit and atomic:**
138
+
139
+ ```ruby
140
+ Dami.flow :user_onboarding do |params|
141
+ step :create_user { db[:users].create(params) }
142
+ step :send_email { Mailer.welcome(user) }
143
+ step :track_event { Analytics.track('signup') }
144
+ step :sync_crm { CRM.sync(user) }
145
+ end
146
+ ```
147
+
148
+ **If any step fails, everything rolls back.** Your database stays consistent. No orphaned records. No mystery states.
149
+
150
+ **The Dami Guarantee:** Atomic workflows or nothing.
151
+
152
+ ---
153
+
154
+ ### **6. Performance by Design**
155
+
156
+ Most ORMs optimize later. Dami benchmarked from line one:
157
+
158
+ ```
159
+ ActiveRecord (includes): 847ms
160
+ Dami (preload): 524ms ← 1.6x faster
161
+
162
+ ActiveRecord (count): 234ms
163
+ Dami (count): 207ms ← Actual SQL COUNT(*)
164
+
165
+ ActiveRecord (create): 892ms
166
+ Dami (create): 743ms ← No object overhead
167
+ ```
168
+
169
+ **These are real benchmarks. Public. Reproducible.**
170
+
171
+ When you eliminate unnecessary abstractions, performance comes free.
172
+
173
+ ---
174
+
175
+ ## When Dami Fits
176
+
177
+ **Perfect for:**
178
+ - Mid-size applications (10K-1M records)
179
+ - International SaaS products
180
+ - Teams that value architecture over feature bloat
181
+ - Developers tired of fighting their tools
182
+
183
+ **Not perfect for:**
184
+ - Enterprise applications needing PostgreSQL today (coming v1.1)
185
+ - Projects requiring Rails' massive gem ecosystem immediately
186
+ - Facebook-scale requirements
187
+ - Full-stack framework needs (Dami is ORM + business logic only)
188
+
189
+ **Dami is a scalpel, not a Swiss Army knife.** For the right job, it's the best tool in Ruby.
190
+
191
+ ---
192
+
193
+ ## The Architectural Philosophy
194
+
195
+ Most frameworks try to be flexible. Dami believes **constraints liberate creativity.**
196
+
197
+ When you can't put code in the wrong place, you stop wasting mental energy on "where does this go?" When validations are infrastructure, consistency is automatic. When business logic is explicit, debugging is straightforward.
198
+
199
+ **Dami doesn't just solve today's problems. It prevents tomorrow's.**
200
+
201
+ The framework that guides you toward better code. The ORM that makes architectural decay impossible. The tool that actually sparks joy.
202
+
203
+ ---
204
+
205
+ ## Production Ready
206
+
207
+ - ✅ **228 comprehensive tests** - Edge cases, concurrency, unicode, NULL handling
208
+ - ✅ **Zero dependencies** - Just SQLite gem
209
+ - ✅ **Battle-tested** - Stress-tested with concurrent operations
210
+ - ✅ **MIT License** - Use it anywhere
211
+ - ✅ **v1.0** - Production stable
212
+
213
+ ---
214
+
215
+ ## Get Started
216
+
217
+ ```bash
218
+ gem install dami
219
+ ```
220
+
221
+ **The Ruby ORM that prevents bad architecture by design.**
222
+
223
+ **Dami. Build better.**
224
+
225
+ ---
226
+
227
+ *Production Ready v1.0 • MIT License • Made for Developers Who Ship*
228
+
229
+ ---
230
+
231
+ # **Website Structure (Revised)**
232
+
233
+ ## **Homepage**
234
+
235
+ ### **Hero Section**
236
+ **H1:** The Ruby ORM That Prevents Bad Architecture
237
+ **Subheadline:** Enforced separation. Built-in i18n. Atomic workflows. Zero compromise.
238
+ **CTA:** `gem install dami`
239
+ **Secondary CTA:** See How It Works →
240
+
241
+ **Hero Visual:** Split screen showing a bloated Rails model transforming into clean Four Pillars code
242
+
243
+ ---
244
+
245
+ ### **Problem Section**
246
+ **H2:** You've Felt This Pain
247
+
248
+ **Three columns:**
249
+
250
+ **1. Fat Models**
251
+ Your `User` class is 2,000 lines. Schema, validations, callbacks, formatters—all tangled together.
252
+
253
+ **2. Callback Hell**
254
+ User saved but email didn't send. Good luck debugging 47 invisible callbacks.
255
+
256
+ **3. Copy-Paste Validations**
257
+ That regex is in 30 models. Need to change it? Better hope you find them all.
258
+
259
+ ---
260
+
261
+ ### **Solution Section**
262
+ **H2:** Dami Solves This. Permanently.
263
+
264
+ **Three-column features:**
265
+
266
+ **1. Enforced Architecture**
267
+ **Badge:** Impossible to Break
268
+ Four Pillars separate concerns at the DSL level. Try to mix them? The framework stops you.
269
+
270
+ **2. Validation Registry**
271
+ **Badge:** Define Once
272
+ Register rules globally. Use them everywhere. Update in one place.
273
+
274
+ **3. Native I18n**
275
+ **Badge:** Built-In
276
+ One DSL for all translations. Auto-detects locale. Zero overhead.
277
+
278
+ **CTA:** Explore the Architecture →
279
+
280
+ ---
281
+
282
+ ### **Code Comparison Section**
283
+ **H2:** Before & After
284
+
285
+ **Split screen:**
286
+
287
+ **Left - Rails:**
288
+ ```ruby
289
+ class User < ApplicationRecord
290
+ validates :email, format: { with: /.../ }
291
+ validates :phone, format: { with: /.../ }
292
+ after_create :send_email
293
+ after_create :track_event
294
+ # 1,947 more lines...
295
+
296
+ def full_name
297
+ "#{first_name} #{last_name}"
298
+ end
299
+ end
300
+ ```
301
+
302
+ **Right - Dami:**
303
+ ```ruby
304
+ # Structure
305
+ Dami.model :users do
306
+ fields { field :email, :string }
307
+ end
308
+
309
+ # Behavior
310
+ Dami.behavior :users do
311
+ validate do
312
+ rule :email, :email
313
+ rule :phone, :phone
314
+ end
315
+ end
316
+
317
+ # Flow
318
+ Dami.flow :signup do |params|
319
+ step(:create) { db[:users].create(params) }
320
+ step(:email) { Mailer.welcome(user) }
321
+ step(:track) { Analytics.track('signup') }
322
+ end
323
+ ```
324
+
325
+ **Callout:** Clean. Testable. Maintainable.
326
+
327
+ ---
328
+
329
+ ### **Six Guarantees Section**
330
+ **H2:** Six Architectural Guarantees
331
+
332
+ **Grid layout (2x3):**
333
+
334
+ 1. **🏗️ Enforced Pillars**
335
+ Framework prevents mixing concerns
336
+
337
+ 2. **🔐 Central Validation**
338
+ Define once, use everywhere
339
+
340
+ 3. **🌍 Native I18n**
341
+ Built-in, not bolted on
342
+
343
+ 4. **✨ Auto Migrations**
344
+ Generate from models, not hand-write
345
+
346
+ 5. **🔄 Atomic Flows**
347
+ Transactional workflows, not callbacks
348
+
349
+ 6. **⚡ Proven Speed**
350
+ 1.6x faster, benchmarked, reproducible
351
+
352
+ ---
353
+
354
+ ### **Migration Magic Section**
355
+ **H2:** Migrations Without the Tedium
356
+
357
+ **Interactive demo:**
358
+
359
+ **Step 1:** Define model
360
+ **Step 2:** Run `dami generate migration`
361
+ **Step 3:** Perfect migration appears
362
+
363
+ **Animation:** Typing field into model → migration file generates automatically
364
+
365
+ **Callout:** Never write a migration by hand again.
366
+
367
+ ---
368
+
369
+ ### **I18n Spotlight**
370
+ **H2:** Built for the Global Web
371
+
372
+ **Live locale switcher demo:**
373
+
374
+ ```ruby
375
+ Dami.db(:users).create(email: '')
376
+ ```
377
+
378
+ **Error output changes with locale:**
379
+ - 🇬🇧 "Email cannot be blank"
380
+ - 🇪🇸 "Email no puede estar en blanco"
381
+ - 🇫🇷 "Email ne peut pas être vide"
382
+
383
+ **Four translation scopes:**
384
+ - Validations
385
+ - Model names
386
+ - Enum values
387
+ - Error messages
388
+
389
+ **Callout:** One codebase. Every language. Zero friction.
390
+
391
+ ---
392
+
393
+ ### **Performance Section**
394
+ **H2:** Speed You Can Prove
395
+
396
+ **Benchmark graph:**
397
+ ```
398
+ Association Preloading:
399
+ ActiveRecord: ████████████████ 847ms
400
+ Dami: █████████ 524ms ← 1.6x faster
401
+ ```
402
+
403
+ **CTA:** Run the Benchmarks Yourself →
404
+
405
+ ---
406
+
407
+ ### **When It Fits Section**
408
+ **H2:** Built For
409
+
410
+ **Grid:**
411
+ - 📊 Mid-size applications (10K-1M records)
412
+ - 🌍 International SaaS products
413
+ - 🏗️ Teams valuing architecture
414
+ - ⚡ Developers who ship
415
+
416
+ **Not Built For:**
417
+ - Enterprise apps needing PostgreSQL today
418
+ - Projects requiring massive gem ecosystems
419
+ - Facebook-scale requirements
420
+
421
+ **Callout:** Honest about what it is. Confident in what it does.
422
+
423
+ ---
424
+
425
+ ### **Social Proof**
426
+ **H2:** Developers Who Switched
427
+
428
+ **Three testimonial cards:**
429
+
430
+ 1. "Finally, an ORM that prevents fat models instead of just warning about them."
431
+ 2. "The validation registry alone is worth switching. Never copy-pasting again."
432
+ 3. "Built-in i18n that actually works. This should be standard."
433
+
434
+ ---
435
+
436
+ ### **Final CTA**
437
+ **H2:** Build Better
438
+
439
+ ```bash
440
+ gem install dami
441
+ ```
442
+
443
+ **Sub-CTA:**
444
+ - Read the Docs →
445
+ - View on GitHub →
446
+ - Join Discord →
447
+
448
+ **Badge:** v1.0 • Production Ready • MIT License
449
+
450
+ ---
451
+
452
+ ## **Key Pages**
453
+
454
+ ### **/architecture**
455
+ Deep dive into Four Pillars with interactive examples
456
+
457
+ ### **/benchmarks**
458
+ Full benchmark suite, methodology, reproducible containers
459
+
460
+ ### **/i18n**
461
+ Complete i18n guide with live locale switching demo
462
+
463
+ ### **/vs-rails**
464
+ Honest comparison: when to use Dami vs Rails
465
+
466
+ ### **/docs**
467
+ Full documentation with searchable API reference
468
+
469
+ ---
470
+
471
+ **Design Notes:**
472
+ - Clean, minimal design
473
+ - Code examples everywhere
474
+ - Interactive demos where possible
475
+ - Fast load times (practice what we preach)
476
+ - Dark mode default (developers love it)
477
+ - Mobile-responsive (but desktop-first)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+ module Dami
3
+ class Command
4
+ def self.requires_context(*keys)
5
+ @required_context = keys
6
+ keys.each { |key| define_method(key) { @context.fetch(key) } }
7
+ end
8
+ def self.validate(name, error:, **conds, &block)
9
+ (@validations ||= []) << { name: name, error: error, if: conds[:if], unless: conds[:unless], block: block }
10
+ end
11
+ def self.transform(name, &block)
12
+ (@transformations ||= []) << { name: name, block: block }
13
+ end
14
+ def self.apply(callable)
15
+ callable.call(self)
16
+ end
17
+ def self.compose(command_class, mapping = {})
18
+ (@composed_commands ||= []) << { command: command_class, mapping: mapping }
19
+ end
20
+ def self.clear_definitions!
21
+ @required_context, @validations, @transformations, @composed_commands = nil, nil, nil, nil
22
+ end
23
+ attr_reader :original, :data, :context, :errors
24
+ def initialize(opts)
25
+ @original = (opts[:original] || {}).transform_keys(&:to_sym)
26
+ @data = opts[:data].transform_keys(&:to_sym)
27
+ @context = opts[:context] || {}
28
+ @errors = {}
29
+ validate_required_context!
30
+ end
31
+ def call
32
+ run_composed_commands
33
+ run_validations
34
+ run_transformations if valid?
35
+ self
36
+ end
37
+ def valid?
38
+ @errors.empty?
39
+ end
40
+ def get(key)
41
+ @data[key]
42
+ end
43
+ def changed?(field)
44
+ sym_field = field.to_sym
45
+ @data.key?(sym_field) && @original[sym_field] != @data[sym_field]
46
+ end
47
+ private
48
+ def validate_required_context!
49
+ missing = (self.class.instance_variable_get(:@required_context) || []) - @context.keys
50
+ raise ArgumentError, "Missing required context: #{missing.join(', ')}" unless missing.empty?
51
+ end
52
+ def run_composed_commands
53
+ (self.class.instance_variable_get(:@composed_commands) || []).each do |c|
54
+ mapped_ctx = c[:mapping].transform_values { |v| @context[v] }
55
+ cmd = c[:command].new(original: @original, data: @data, context: @context.merge(mapped_ctx)).call
56
+ @errors.merge!(cmd.errors) { |_, old, new| old + new } unless cmd.valid?
57
+ @data = cmd.data # Carry over transformed data from composed command
58
+ end
59
+ end
60
+ def run_validations
61
+ (self.class.instance_variable_get(:@validations) || []).each do |v|
62
+ next if v[:if] && !instance_exec(&v[:if])
63
+ next if v[:unless] && instance_exec(&v[:unless])
64
+ unless instance_exec(&v[:block])
65
+ (@errors[v[:name]] ||= []) << v[:error]
66
+ end
67
+ end
68
+ end
69
+ def run_transformations
70
+ current_data = @data.dup
71
+ (self.class.instance_variable_get(:@transformations) || []).each do |t|
72
+ # THE FIX: Each transform block returns a hash of changes.
73
+ # We must MERGE these changes into the current data hash.
74
+ result = instance_exec(current_data, &t[:block])
75
+ current_data.merge!(result) if result.is_a?(Hash)
76
+ end
77
+ @data = current_data
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ require 'timeout'
3
+
4
+ module Dami
5
+ class FlowContext
6
+ attr_reader :params
7
+ def initialize(params)
8
+ @params = params
9
+ @after_commit_hooks = []
10
+ define_param_accessors
11
+ end
12
+ def prepare(command_class, with:, original: nil)
13
+ command = command_class.new(original: original, data: with, context: @params)
14
+ command.call
15
+ halt(Dami::Failure.new(command.errors)) unless command.valid?
16
+ command.data
17
+ end
18
+ # THE FIX IS HERE: Renamed 'retry:' to 'retry_options:'
19
+ def perform(name, retry_options: {}, timeout: nil, &block)
20
+ action = -> do
21
+ # THE FIX: Use the new parameter name 'retry_options'
22
+ retry_exceptions = Array(retry_options[:on])
23
+ # Add 1 for the initial attempt.
24
+ max_attempts = (retry_options[:times] || 0) + 1
25
+ attempts = 0
26
+ begin
27
+ attempts += 1
28
+ block.call
29
+ rescue *retry_exceptions => e
30
+ raise if attempts >= max_attempts
31
+ retry # This 'retry' keyword is correct because it's inside the rescue block
32
+ end
33
+ end
34
+ if timeout
35
+ Timeout.timeout(timeout) { action.call }
36
+ else
37
+ action.call
38
+ end
39
+ end
40
+ def succeed(with:, and_then: [])
41
+ @after_commit_hooks.concat(Array(and_then))
42
+ halt(Dami::Success.new(with))
43
+ end
44
+ def run(flow_name, **params)
45
+ Dami.run(flow_name, **(@params.merge(params)))
46
+ end
47
+ def db(model_name)
48
+ Dami.db(model_name)
49
+ end
50
+ def halt(value)
51
+ throw :halt, value
52
+ end
53
+ def run_after_commit_hooks!
54
+ @after_commit_hooks.each(&:call)
55
+ end
56
+ private
57
+ def define_param_accessors
58
+ @params.each_key do |key|
59
+ define_singleton_method(key) { @params[key] }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ module Dami
3
+ class Draft
4
+ attr_reader :original, :data, :errors, :context
5
+ def initialize(original:, data:, context: {})
6
+ @original = (original || {}).freeze
7
+ @data = data
8
+ @context = context
9
+ @errors = {}
10
+ end
11
+ def verify(name, error:, &block)
12
+ return unless instance_exec(&block) == false
13
+ (@errors[name] ||= []) << error
14
+ end
15
+ def transform(name, &block)
16
+ @data = instance_exec(@data.dup, &block)
17
+ end
18
+ def apply(callable)
19
+ callable.call(self)
20
+ end
21
+ def prevent_changes(*fields, message: "cannot be changed")
22
+ fields.each do |field|
23
+ verify(field, error: message) { !changed?(field) }
24
+ end
25
+ end
26
+ def changed?(field)
27
+ @data.key?(field) && @original[field] != @data[field]
28
+ end
29
+ def get(key)
30
+ @data[key]
31
+ end
32
+ def valid?
33
+ @errors.empty?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+
2
+
3
+ # frozen_string_literal: true
4
+ module Dami
5
+ def self.flow(name, &block)
6
+ (@flows ||= {})[name] = Flow.new(block)
7
+ end
8
+
9
+ def self.find_flow(name)
10
+ (@flows || {}).fetch(name) { raise "Flow :#{name} not defined" }
11
+ end
12
+
13
+ class Flow
14
+ def initialize(block)
15
+ @block = block
16
+ @rescue_handlers = {}
17
+ @after_commit_hooks = []
18
+ end
19
+
20
+ # Fix: Ensure the method accepts the context parameter
21
+ def call(context)
22
+ context.instance_exec(&@block)
23
+ rescue => e
24
+ handle_exception(e, context)
25
+ end
26
+
27
+ def run_after_commit_hooks
28
+ @after_commit_hooks.each(&:call)
29
+ end
30
+
31
+ def rescue_from(exception_class, &handler)
32
+ @rescue_handlers[exception_class] = handler
33
+ end
34
+
35
+ private
36
+
37
+ def handle_exception(exception, context)
38
+ handler = @rescue_handlers.find { |klass, _| exception.is_a?(klass) }&.last
39
+ handler ? handler.call(exception, context) : raise(exception)
40
+ end
41
+ end
42
+ end