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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +306 -0
- data/LICENSE +21 -0
- data/README.md +106 -0
- data/bin/dami +10 -0
- data/docs/01.Getting_Started.md +220 -0
- data/docs/02.Models_and_Fields.md +244 -0
- data/docs/03.Querying.md +387 -0
- data/docs/04.Creating_Updating_Deleting.md +226 -0
- data/docs/05.Validation.md +259 -0
- data/docs/06.Protection.md +160 -0
- data/docs/07.Associations.md +239 -0
- data/docs/08.Scopes.md +99 -0
- data/docs/09.Migrations.md +165 -0
- data/docs/10.Flows_and_Commands.md +181 -0
- data/docs/11.Localization.md +435 -0
- data/docs/12.TheDamiWay.md +227 -0
- data/docs/Manifesto.md +305 -0
- data/docs/site.md +477 -0
- data/lib/dami/actions/command.rb +80 -0
- data/lib/dami/actions/context.rb +63 -0
- data/lib/dami/actions/draft.rb +36 -0
- data/lib/dami/actions/flow.rb +42 -0
- data/lib/dami/adapters/base.rb +40 -0
- data/lib/dami/adapters/sqlite/connection.rb +122 -0
- data/lib/dami/adapters/sqlite/core.rb +17 -0
- data/lib/dami/adapters/sqlite/query.rb +353 -0
- data/lib/dami/adapters/sqlite/schema.rb +135 -0
- data/lib/dami/cli.rb +117 -0
- data/lib/dami/configuration.rb +246 -0
- data/lib/dami/core.rb +98 -0
- data/lib/dami/dsl_guardrails.rb +45 -0
- data/lib/dami/errors.rb +89 -0
- data/lib/dami/inflector.rb +245 -0
- data/lib/dami/localization.rb +131 -0
- data/lib/dami/migration.rb +84 -0
- data/lib/dami/migrator.rb +105 -0
- data/lib/dami/plugins/associations.rb +284 -0
- data/lib/dami/plugins/nested_attributes.rb +180 -0
- data/lib/dami/plugins/protection.rb +30 -0
- data/lib/dami/plugins/validations.rb +90 -0
- data/lib/dami/query/builder.rb +132 -0
- data/lib/dami/query/enumerable.rb +62 -0
- data/lib/dami/query/persistence.rb +133 -0
- data/lib/dami/record_proxy.rb +32 -0
- data/lib/dami/result.rb +27 -0
- data/lib/dami/schema/diff.rb +84 -0
- data/lib/dami/schema/dumper.rb +60 -0
- data/lib/dami/schema/generator.rb +69 -0
- data/lib/dami/schema/introspector.rb +34 -0
- data/lib/dami/schema/loader.rb +58 -0
- data/lib/dami/schema.rb +13 -0
- data/lib/dami/validation_rules.rb +72 -0
- data/lib/dami/version.rb +3 -0
- data/lib/dami.rb +32 -0
- 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
|