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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# Chapter 11: Internationalization (I18n) & Localization
|
|
2
|
+
|
|
3
|
+
Building a world-class application means speaking your users' language. Internationalization (i18n) is often a complex afterthought, involving scattered files and inconsistent APIs. Dami treats it as a first-class citizen with a simple, powerful philosophy: **all user-facing text that the framework is aware of should live in one clean, consistent, and localizable place.**
|
|
4
|
+
|
|
5
|
+
This is achieved through a new, core DSL: `Dami.localize`.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
This unified system is not just for validation messages. It's a complete solution for translating every piece of "chrome" text connected to your data layer, providing a single source of truth for your application's vocabulary.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The `Dami.localize` DSL
|
|
14
|
+
|
|
15
|
+
The `Dami.localize` block is the central hub for all framework-aware translations. You define a scope (like `:validations` or `:models`), specify a language, and provide your translations using a clean, block-based syntax.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# config/locales/dami.rb
|
|
19
|
+
|
|
20
|
+
Dami.localize :validations do
|
|
21
|
+
en do
|
|
22
|
+
set :required, "cannot be blank"
|
|
23
|
+
set :min_length, ->(min) { "is too short (minimum: #{min} characters)" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
es do
|
|
27
|
+
set :required, "no puede estar en blanco"
|
|
28
|
+
set :min_length, ->(min) { "es demasiado corto (mínimo: #{min} caracteres)" }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
````
|
|
32
|
+
|
|
33
|
+
This approach is powerful because it's pure Ruby. You can load these definitions from anywhere, and it's completely explicit—no "magic" `method_missing`.
|
|
34
|
+
|
|
35
|
+
-----
|
|
36
|
+
|
|
37
|
+
## Why `Dami.localize` is More Than Just Validations
|
|
38
|
+
|
|
39
|
+
Your ORM is the single source of truth for your data's structure. As such, it's the perfect place to manage the human-readable text associated with that structure. `Dami.localize` is designed to be a holistic system, covering four key scopes.
|
|
40
|
+
|
|
41
|
+
### 1\. `:validations` - Error Messages
|
|
42
|
+
|
|
43
|
+
This is the most common use case. It allows you to provide clean, translated error messages for all of Dami's built-in and custom validation rules.
|
|
44
|
+
|
|
45
|
+
* **The Problem:** Hardcoded English error messages scattered throughout your models or initializers.
|
|
46
|
+
* **The Solution:** A central `validations` scope that the `Dami.behavior` block will automatically use.
|
|
47
|
+
|
|
48
|
+
<!-- end list -->
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Dami.localize :validations do
|
|
52
|
+
en { set :email, "must be a valid email address" }
|
|
53
|
+
es { set :email, "debe ser una dirección de correo electrónico válida" }
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2\. `:models` - Attribute & Model Names
|
|
58
|
+
|
|
59
|
+
* **The Problem:** Your database uses `first_name`, but your HTML form label should say "First name". This couples your views to your schema's raw column names.
|
|
60
|
+
* **The Solution:** A `models` scope to provide canonical, human-readable names for your models and their attributes.
|
|
61
|
+
|
|
62
|
+
<!-- end list -->
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
Dami.localize :models do
|
|
66
|
+
en do
|
|
67
|
+
attributes_for :users do
|
|
68
|
+
set :model_name, "User"
|
|
69
|
+
set :first_name, "First Name"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
es do
|
|
73
|
+
attributes_for :users do
|
|
74
|
+
set :model_name, "Usuario"
|
|
75
|
+
set :first_name, "Nombre"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A future helper would make this easy to use in views:
|
|
81
|
+
# Dami.human_attribute_name(:users, :first_name, locale: :es) # => "Nombre"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3\. `:enums` - Stored Values
|
|
85
|
+
|
|
86
|
+
* **The Problem:** Your database stores a post's status as `"published"`, but you want to display "Published" or "Publicado" to the user.
|
|
87
|
+
* **The Solution:** An `enums` scope to map raw data values to clean, display-friendly text.
|
|
88
|
+
|
|
89
|
+
<!-- end list -->
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
Dami.localize :enums do
|
|
93
|
+
en do
|
|
94
|
+
enum_for :posts, :status do
|
|
95
|
+
set :published, "Published"
|
|
96
|
+
set :draft, "In Draft"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 4\. `:errors` - System Exceptions
|
|
103
|
+
|
|
104
|
+
* **The Problem:** Dami might raise a `Dami::ProtectionError`. You want to rescue this and show a friendly, translated message to the user.
|
|
105
|
+
* **The Solution:** An `errors` scope to provide user-facing translations for framework-level exceptions.
|
|
106
|
+
|
|
107
|
+
<!-- end list -->
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
Dami.localize :errors do
|
|
111
|
+
en { set :protection_error, "You are not authorized to change one or more of the submitted fields." }
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
-----
|
|
116
|
+
|
|
117
|
+
## Locale Management
|
|
118
|
+
|
|
119
|
+
Dami is designed to integrate seamlessly with the existing Ruby ecosystem. It uses a smart, three-tiered approach to determine the current language.
|
|
120
|
+
|
|
121
|
+
1. **Manual Override:** You can force a process-wide default with `Dami.locale = :es` (set it once at boot). Inside a web request use `Dami.thread_locale = :es` or `Dami.with_locale` instead — those are thread-local, so one request's locale never leaks into another thread's.
|
|
122
|
+
2. **Automatic `I18n` Detection:** If the standard `i18n` gem is loaded, Dami will **automatically** use `I18n.locale`. This means it works out-of-the-box with Rails, Sinatra, and most other Ruby web frameworks.
|
|
123
|
+
3. **Default:** If neither of the above is present, it defaults to `:en`.
|
|
124
|
+
|
|
125
|
+
For testing or handling specific requests, you can use the `Dami.with_locale` helper, which guarantees the original locale is restored. It is thread-local: only the calling thread sees the temporary locale.
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
Dami.with_locale(:es) do
|
|
129
|
+
# All Dami operations inside this block will use the Spanish locale.
|
|
130
|
+
puts Dami.current_locale # => :es
|
|
131
|
+
end
|
|
132
|
+
puts Dami.current_locale # => :en (back to the original)
|
|
133
|
+
```
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Helper Methods for Views and Forms
|
|
137
|
+
|
|
138
|
+
Dami provides convenient helper methods to retrieve human-readable labels for your models and fields. These are essential for building forms, table headers, and any UI that displays field names to users.
|
|
139
|
+
|
|
140
|
+
### `Dami.localize_field` (alias: `lf`)
|
|
141
|
+
|
|
142
|
+
Retrieves the translated label for a specific field on a model.
|
|
143
|
+
```ruby
|
|
144
|
+
# Define your translations
|
|
145
|
+
Dami.localize :models do
|
|
146
|
+
en do
|
|
147
|
+
attributes_for :users do
|
|
148
|
+
set :first_name, "First Name"
|
|
149
|
+
set :email, "Email Address"
|
|
150
|
+
set :phone_number, "Phone"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
es do
|
|
154
|
+
attributes_for :users do
|
|
155
|
+
set :first_name, "Nombre"
|
|
156
|
+
set :email, "Correo Electrónico"
|
|
157
|
+
set :phone_number, "Teléfono"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# In your views or controllers
|
|
163
|
+
Dami.localize_field(:users, :first_name) # => "First Name" (or "Nombre" if locale is :es)
|
|
164
|
+
Dami.lf(:users, :email) # => "Email Address" (shorthand)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Real-world example in a form:**
|
|
168
|
+
```ruby
|
|
169
|
+
# In a Sinatra/Hanami view
|
|
170
|
+
<form>
|
|
171
|
+
<label><%= Dami.lf(:users, :first_name) %>:</label>
|
|
172
|
+
<input type="text" name="user[first_name]">
|
|
173
|
+
|
|
174
|
+
<label><%= Dami.lf(:users, :email) %>:</label>
|
|
175
|
+
<input type="email" name="user[email]">
|
|
176
|
+
</form>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
When a Spanish-speaking user visits, the labels automatically display as "Nombre" and "Correo Electrónico".
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
### `Dami.localize_model` (alias: `lm`)
|
|
183
|
+
|
|
184
|
+
Retrieves the translated name for a model itself. Useful for page titles, breadcrumbs, and headers.
|
|
185
|
+
```ruby
|
|
186
|
+
Dami.localize :models do
|
|
187
|
+
en do
|
|
188
|
+
attributes_for :users do
|
|
189
|
+
set :model_name, "User"
|
|
190
|
+
end
|
|
191
|
+
attributes_for :products do
|
|
192
|
+
set :model_name, "Product"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
es do
|
|
196
|
+
attributes_for :users do
|
|
197
|
+
set :model_name, "Usuario"
|
|
198
|
+
end
|
|
199
|
+
attributes_for :products do
|
|
200
|
+
set :model_name, "Producto"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# In your views
|
|
206
|
+
Dami.localize_model(:users) # => "User" (or "Usuario" if locale is :es)
|
|
207
|
+
Dami.lm(:products) # => "Product" (shorthand)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Real-world example in a page header:**
|
|
211
|
+
```ruby
|
|
212
|
+
# In a controller
|
|
213
|
+
class UsersController
|
|
214
|
+
def index
|
|
215
|
+
@model_name = Dami.lm(:users)
|
|
216
|
+
# Renders "Users" or "Usuarios" depending on locale
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# In the view
|
|
221
|
+
<h1><%= @model_name %> Directory</h1>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
### Explicit Locale Override
|
|
226
|
+
|
|
227
|
+
Both helpers accept an optional `locale:` parameter to override the current locale:
|
|
228
|
+
```ruby
|
|
229
|
+
Dami.locale = :en
|
|
230
|
+
|
|
231
|
+
# Force Spanish translation
|
|
232
|
+
Dami.lf(:users, :first_name, locale: :es) # => "Nombre"
|
|
233
|
+
Dami.lm(:users, locale: :es) # => "Usuario"
|
|
234
|
+
|
|
235
|
+
# Current locale still English
|
|
236
|
+
Dami.lf(:users, :first_name) # => "First Name"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This is useful for:
|
|
240
|
+
- Admin interfaces showing translations for review
|
|
241
|
+
- Email templates that need to use the recipient's preferred language
|
|
242
|
+
- Testing different locale outputs
|
|
243
|
+
|
|
244
|
+
### Automatic Locale Detection
|
|
245
|
+
|
|
246
|
+
The helpers automatically use the current locale through the three-tier detection system:
|
|
247
|
+
|
|
248
|
+
1. **Manual override**: `Dami.locale = :es`
|
|
249
|
+
2. **I18n gem**: `I18n.locale` (automatic in Rails/Sinatra)
|
|
250
|
+
3. **Default**: `:en`
|
|
251
|
+
|
|
252
|
+
**In a Rails controller:**
|
|
253
|
+
```ruby
|
|
254
|
+
class ProductsController < ApplicationController
|
|
255
|
+
def show
|
|
256
|
+
# Rails sets I18n.locale per-request based on user preference
|
|
257
|
+
# Dami automatically picks it up
|
|
258
|
+
|
|
259
|
+
@product_name_label = Dami.lf(:products, :name)
|
|
260
|
+
# => "Product Name" for English users
|
|
261
|
+
# => "Nombre del Producto" for Spanish users
|
|
262
|
+
# Zero configuration needed!
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Building Dynamic Forms
|
|
268
|
+
|
|
269
|
+
Here's a complete example of using these helpers to build a multi-lingual form:
|
|
270
|
+
```ruby
|
|
271
|
+
# Define all your translations
|
|
272
|
+
Dami.localize :models do
|
|
273
|
+
en do
|
|
274
|
+
attributes_for :products do
|
|
275
|
+
set :model_name, "Product"
|
|
276
|
+
set :name, "Product Name"
|
|
277
|
+
set :description, "Description"
|
|
278
|
+
set :price, "Price"
|
|
279
|
+
set :category, "Category"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
es do
|
|
283
|
+
attributes_for :products do
|
|
284
|
+
set :model_name, "Producto"
|
|
285
|
+
set :name, "Nombre del Producto"
|
|
286
|
+
set :description, "Descripción"
|
|
287
|
+
set :price, "Precio"
|
|
288
|
+
set :category, "Categoría"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# In your view template
|
|
294
|
+
<h1>New <%= Dami.lm(:products) %></h1>
|
|
295
|
+
|
|
296
|
+
<form action="/products" method="post">
|
|
297
|
+
<div class="field">
|
|
298
|
+
<label><%= Dami.lf(:products, :name) %></label>
|
|
299
|
+
<input type="text" name="product[name]">
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div class="field">
|
|
303
|
+
<label><%= Dami.lf(:products, :description) %></label>
|
|
304
|
+
<textarea name="product[description]"></textarea>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="field">
|
|
308
|
+
<label><%= Dami.lf(:products, :price) %></label>
|
|
309
|
+
<input type="number" name="product[price]">
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<button>Create <%= Dami.lm(:products) %></button>
|
|
313
|
+
</form>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
This same template renders perfectly in any language you've defined, with zero code changes.
|
|
317
|
+
|
|
318
|
+
### Best Practices
|
|
319
|
+
|
|
320
|
+
1. **Define labels even for English**: Don't assume field names are self-explanatory. `email` should be "Email Address", not just "email".
|
|
321
|
+
|
|
322
|
+
2. **Use shorthand in views**: `lf` and `lm` are shorter and cleaner in templates where you'll use them frequently.
|
|
323
|
+
|
|
324
|
+
3. **Consistent naming**: Use the same field names across all locales to make translation maintenance easier.
|
|
325
|
+
|
|
326
|
+
4. **Fallback gracefully**: If a translation is missing, the helper returns `nil`. Handle this in your views:
|
|
327
|
+
```ruby
|
|
328
|
+
label = Dami.lf(:users, :field_name) || "Field Name"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
-----
|
|
332
|
+
|
|
333
|
+
## Putting It All Together: An End-to-End Example
|
|
334
|
+
|
|
335
|
+
This example shows how the entire system works together, from definition to execution.
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
# 1. Define your translations
|
|
339
|
+
Dami.localize :validations do
|
|
340
|
+
en { set :required, "cannot be blank" }
|
|
341
|
+
es { set :required, "no puede estar en blanco" }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# 2. Define your model and behavior
|
|
345
|
+
Dami.model(:articles) { fields { field :title, :string } }
|
|
346
|
+
Dami.behavior(:articles) { validate { rule :title, :required } }
|
|
347
|
+
|
|
348
|
+
# 3. Trigger the validation in English (default)
|
|
349
|
+
error_en = assert_raises(Dami::ValidationError) do
|
|
350
|
+
Dami.db(:articles).create(title: '')
|
|
351
|
+
end
|
|
352
|
+
assert_includes error_en.errors[:title], "cannot be blank"
|
|
353
|
+
|
|
354
|
+
# 4. Trigger the validation in Spanish
|
|
355
|
+
Dami.with_locale(:es) do
|
|
356
|
+
error_es = assert_raises(Dami::ValidationError) do
|
|
357
|
+
Dami.db(:articles).create(title: '')
|
|
358
|
+
end
|
|
359
|
+
assert_includes error_es.errors[:title], "no puede estar en blanco"
|
|
360
|
+
end
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
-----
|
|
364
|
+
|
|
365
|
+
## Integrating with `I18n` and Other Gems
|
|
366
|
+
|
|
367
|
+
Dami's localization system is designed to be a great citizen in the Ruby ecosystem. It integrates seamlessly with standard tools like the `i18n` gem, so you don't have to choose between Dami's power and your existing workflow.
|
|
368
|
+
|
|
369
|
+
### The "It Just Works" Principle: Automatic `I18n` Detection
|
|
370
|
+
|
|
371
|
+
For over 90% of use cases, **you don't need to do anything.**
|
|
372
|
+
|
|
373
|
+
If Dami detects that the `i18n` gem is loaded in your application (as it is in all Rails, Sinatra, and Hanami apps), it will automatically use `I18n.locale` to determine the current language. When a user's locale is set to `:es` in a web request, Dami's validation messages will automatically be in Spanish.
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
# In a Rails or Sinatra controller
|
|
377
|
+
I18n.with_locale(:es) do
|
|
378
|
+
begin
|
|
379
|
+
# This will now raise a ValidationError with the Spanish message
|
|
380
|
+
# because Dami automatically respects I18n.locale.
|
|
381
|
+
Dami.db(:articles).create!(title: '')
|
|
382
|
+
rescue Dami::ValidationError => e
|
|
383
|
+
# e.errors[:title] will be ["no puede estar en blanco"]
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Bridging the Gap: Loading YAML Files into Dami
|
|
389
|
+
|
|
390
|
+
You don't need to rewrite all your existing translations. You can easily load standard `i18n` YAML files directly into Dami's localization system in an initializer. This gives you a single source of truth for all your application's text.
|
|
391
|
+
|
|
392
|
+
Imagine you have a standard Rails `es.yml` file:
|
|
393
|
+
|
|
394
|
+
```yaml
|
|
395
|
+
# config/locales/es.yml
|
|
396
|
+
es:
|
|
397
|
+
dami:
|
|
398
|
+
validations:
|
|
399
|
+
required: "no puede estar en blanco (desde YAML)"
|
|
400
|
+
models:
|
|
401
|
+
user:
|
|
402
|
+
first_name: "Nombre (desde YAML)"
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
You can load this into Dami in a single, clean step:
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
# config/initializers/dami_i18n.rb
|
|
409
|
+
require 'yaml'
|
|
410
|
+
|
|
411
|
+
# Load the YAML file
|
|
412
|
+
translations = YAML.load_file('config/locales/es.yml')
|
|
413
|
+
dami_translations = translations['es']['dami']
|
|
414
|
+
|
|
415
|
+
# Feed the translations into Dami's DSL
|
|
416
|
+
Dami.localize :validations do
|
|
417
|
+
es do
|
|
418
|
+
dami_translations['validations'].each do |key, value|
|
|
419
|
+
set key.to_sym, value
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
Dami.localize :models do
|
|
425
|
+
es do
|
|
426
|
+
attributes_for :user do
|
|
427
|
+
dami_translations['models']['user'].each do |key, value|
|
|
428
|
+
set key.to_sym, value
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
This pattern allows you to maintain your translations in the standard `i18n` format while still benefiting from Dami's fast, type-safe localization engine at runtime. It's the best of both worlds.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Absolutely. This is the perfect way to end the documentation—a confident, opinionated "manifesto" that clearly articulates Dami's superior design philosophy. Your draft is a great start, and we can definitely crank up the energy to get developers hyped.
|
|
2
|
+
|
|
3
|
+
You're right, the draft is missing the knockout punches about validations and other key differentiators. Let's upgrade this from a simple comparison to a powerful final statement.
|
|
4
|
+
|
|
5
|
+
Here is the revised, high-energy final chapter.
|
|
6
|
+
|
|
7
|
+
-----
|
|
8
|
+
|
|
9
|
+
````markdown
|
|
10
|
+
# Chapter 12: The Dami Way - A Manifesto
|
|
11
|
+
|
|
12
|
+
You've learned the "what" of Dami's features. This final chapter is about the "why." It's a declaration of the principles that make Dami a fundamentally better way to build modern, maintainable applications.
|
|
13
|
+
|
|
14
|
+
Dami was not created to be another clone in a sea of ORMs. It was forged from experience, designed to eliminate the common pain points and architectural decay that plague traditional frameworks. This philosophy is **"The Dami Way,"** and it's built on four pillars: **Clarity, Architectural Purity, Performance, and Ergonomic Joy.**
|
|
15
|
+
|
|
16
|
+
This chapter is a "shootout" that compares Dami's design choices to the conventional approach (represented by Rails/ActiveRecord). By the end, it will be blatantly obvious why Dami is a superior choice.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Architectural Purity: The Decisive End of the "Fat Model"
|
|
23
|
+
|
|
24
|
+
The single biggest source of technical debt in most web applications is the "fat model"—a single file that becomes a sprawling, thousand-line dumping ground for schema, business logic, query logic, and presentation helpers. It's impossible to read, a nightmare to test, and actively resists change.
|
|
25
|
+
|
|
26
|
+
#### **The Old Way: The Monolithic Model**
|
|
27
|
+
|
|
28
|
+
In a typical Rails application, every concern is crammed into a single `ApplicationRecord` class. This convenience comes at a devastating long-term cost.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# The "Fat Model" Anti-Pattern in Rails
|
|
32
|
+
class User < ApplicationRecord
|
|
33
|
+
# --- Structure, Behavior, Querying, and Presentation are all mixed ---
|
|
34
|
+
has_many :posts
|
|
35
|
+
|
|
36
|
+
validates :email, presence: true, uniqueness: true
|
|
37
|
+
before_save :downcase_email # A callback... but where is it defined?
|
|
38
|
+
|
|
39
|
+
scope :active, -> { where(status: 'active') }
|
|
40
|
+
|
|
41
|
+
def full_name
|
|
42
|
+
"#{first_name} #{last_name}"
|
|
43
|
+
end
|
|
44
|
+
# ... 500 more lines of tangled logic ...
|
|
45
|
+
end
|
|
46
|
+
````
|
|
47
|
+
|
|
48
|
+
#### **The Dami Way: The Four Pillars of Clarity**
|
|
49
|
+
|
|
50
|
+
Dami makes this architectural mess impossible **by design**. It provides four distinct, purpose-built DSLs. This isn't a suggestion; it's a core feature enforced by the framework itself.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# The Dami Way: Clean, Enforced Separation of Concerns
|
|
54
|
+
|
|
55
|
+
# --- 1. Structure (The Skeleton) ---
|
|
56
|
+
Dami.model(:users) { fields { ... }; relationships { ... } }
|
|
57
|
+
|
|
58
|
+
# --- 2. Behavior (The Internal Rules) ---
|
|
59
|
+
Dami.behavior(:users) { validate { ... }; before(:save) { ... } }
|
|
60
|
+
|
|
61
|
+
# --- 3. Presentation (The Public Interface) ---
|
|
62
|
+
Dami.present(:users) { def full_name; ...; end }
|
|
63
|
+
|
|
64
|
+
# --- 4. Querying (The Collection Logic) ---
|
|
65
|
+
Dami.scopes(:users) { scope :active, -> { ... } }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### **The Shootout: Why Dami Wins Decisively**
|
|
69
|
+
|
|
70
|
+
* **Clarity & Discoverability:** When you open a Dami model file, the architecture is immediately obvious. You know exactly where to find schema, write-side rules, read-side helpers, and query logic. No more hunting for a `scope` buried between a dozen private methods.
|
|
71
|
+
* **Superior Testability:** Each pillar can be tested in isolation. You can unit test your presenter methods with plain Ruby hashes, without ever touching the database. You can test your behavior rules without mocking query scopes. This leads to faster, more reliable, and more focused tests.
|
|
72
|
+
* **Architectural Guardrails:** Dami actively prevents architectural decay. You *can't* put a validation rule in your `model` block. The framework will stop you. These guardrails ensure the codebase stays clean and understandable as it grows and new developers join the team.
|
|
73
|
+
|
|
74
|
+
-----
|
|
75
|
+
|
|
76
|
+
## 2\. Validations: A Central Registry vs. Ad-Hoc Chaos
|
|
77
|
+
|
|
78
|
+
Validations are critical, but in most frameworks, they are a source of constant repetition and inconsistency.
|
|
79
|
+
|
|
80
|
+
#### **The Old Way: Repetitive, Scattered Definitions**
|
|
81
|
+
|
|
82
|
+
In Rails, if you need a custom `phone_number` validation, you have to define it ad-hoc in every single model that needs it, often with slightly different regexes and error messages.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# The Repetitive Anti-Pattern in Rails
|
|
86
|
+
class User < ApplicationRecord
|
|
87
|
+
validates :phone, format: { with: /\A\d{10}\z/, message: "must be 10 digits" }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Company < ApplicationRecord
|
|
91
|
+
validates :main_phone, format: { with: /\A\d{3}-\d{3}-\d{4}\z/, message: "use XXX-XXX-XXXX format" }
|
|
92
|
+
end # ❌ Inconsistent logic and messages!
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### **The Dami Way: The Central Validation Registry**
|
|
96
|
+
|
|
97
|
+
Dami treats validations as a first-class, reusable component of your application. You define a rule **once** in a central registry and reuse it everywhere.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# The Dami Way: Define Once, Reuse Everywhere
|
|
101
|
+
|
|
102
|
+
# 1. Register your custom rule
|
|
103
|
+
Dami.rules :default, {
|
|
104
|
+
phone: {
|
|
105
|
+
check: ->(v) { v.to_s.gsub(/\D/, '').length == 10 },
|
|
106
|
+
message: "must be a valid 10-digit phone number"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# 2. Use it by its simple, declarative name
|
|
111
|
+
Dami.behavior(:users) { validate { rule :phone, :phone } }
|
|
112
|
+
Dami.behavior(:companies) { validate { rule :main_phone, :phone } } # ✅ Consistent logic and message!
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### **The Shootout: Why Dami Wins Decisively**
|
|
116
|
+
|
|
117
|
+
* **DRY (Don't Repeat Yourself):** Dami's registry eliminates the possibility of having dozens of slightly different, copy-pasted validations littered across your codebase.
|
|
118
|
+
* **Consistency:** It guarantees that the logic and error message for a given rule are identical everywhere, providing a consistent user experience.
|
|
119
|
+
* **Maintainability:** If your business rule for a "phone number" changes, you update it in **one single place**, and the entire application is instantly updated. This is a massive win for long-term maintenance.
|
|
120
|
+
|
|
121
|
+
-----
|
|
122
|
+
|
|
123
|
+
## 3\. Business Logic: Explicit Orchestration vs. "Callback Hell"
|
|
124
|
+
|
|
125
|
+
Complex business processes are the heart of any application. Dami provides a dedicated, top-tier tool for this, while other frameworks rely on a pattern that is notoriously brittle and opaque.
|
|
126
|
+
|
|
127
|
+
#### **The Old Way: Implicit Callback Chains ("Callback Hell")**
|
|
128
|
+
|
|
129
|
+
A user signup process in Rails often becomes an invisible, untestable chain of `after_create` callbacks. When it breaks, it's nearly impossible to debug.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# The "Callback Hell" Anti-Pattern
|
|
133
|
+
class User < ApplicationRecord
|
|
134
|
+
after_create :send_welcome_email, :sync_to_crm, :assign_to_default_team
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# In the controller:
|
|
138
|
+
def create
|
|
139
|
+
@user = User.new(user_params)
|
|
140
|
+
if @user.save # ❌ What happens here? An invisible, multi-step process kicks off.
|
|
141
|
+
# What if sync_to_crm fails? The user is already created,
|
|
142
|
+
# the email is sent, but the system is now in an inconsistent state.
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### **The Dami Way: An Explicit, Transactional Flow**
|
|
148
|
+
|
|
149
|
+
Dami's **Flows** make the entire business process visible, atomic, and safe. It reads like a clear, imperative script for your most critical workflows.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# The Dami Way: An Explicit, Readable, and Resilient Script
|
|
153
|
+
Dami.run(:user_signup_flow) do
|
|
154
|
+
# 1. Validate and prepare all incoming data
|
|
155
|
+
clean_data = prepare CreateUserCommand, with: params
|
|
156
|
+
|
|
157
|
+
# 2. Run all database operations inside a single, atomic transaction
|
|
158
|
+
user = db(:users).create(clean_data[:user])
|
|
159
|
+
db(:teams).default.add_member(user)
|
|
160
|
+
|
|
161
|
+
# 3. Perform external actions with built-in resilience
|
|
162
|
+
perform "Sync to CRM", retry_options: { on: [Crm::ApiError], times: 2 } do
|
|
163
|
+
CrmService.sync_user(user)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 4. Safely enqueue side effects ONLY AFTER the transaction commits
|
|
167
|
+
succeed with: user, and_then: [
|
|
168
|
+
WelcomeEmailJob.with(user_id: user[:id])
|
|
169
|
+
]
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### **The Shootout: Why Dami Wins Decisively**
|
|
174
|
+
|
|
175
|
+
* **Visibility & Clarity:** The Dami Flow is a self-documenting script. Any developer can read it top-to-bottom and understand the entire business process. There are no hidden actions.
|
|
176
|
+
* **Transactional Integrity:** If the `CrmService` call fails, the entire database transaction is **automatically rolled back**. The user and team membership are never saved. The system remains in a consistent state, eliminating an entire class of bugs.
|
|
177
|
+
* **Safety for Side Effects:** The `and_then:` block is a transaction-aware gatekeeper. It guarantees that irreversible actions like sending an email or charging a credit card happen **only after** all database operations have been successfully and permanently committed. This solves the infamous "welcome email sent to a ghost user" bug.
|
|
178
|
+
|
|
179
|
+
-----
|
|
180
|
+
|
|
181
|
+
### A Caveat: When *Not* to Use Dami (The Honest Guide)
|
|
182
|
+
|
|
183
|
+
Every great tool is defined as much by what it *doesn't* do as by what it does. Dami is an opinionated framework designed for a specific philosophy. We believe it's a better way to build, but like any sharp tool, it's not the right one for every single job.
|
|
184
|
+
|
|
185
|
+
Being open about these trade-offs is critical. Dami might **not** be the best fit for your project if:
|
|
186
|
+
|
|
187
|
+
#### 1. ...You Need a Massive, Established Ecosystem Out of the Box.
|
|
188
|
+
|
|
189
|
+
The Rails and ActiveRecord ecosystem is a vast, mature jungle of gems and plugins for everything from authentication (`Devise`) to admin panels (`ActiveAdmin`). This is an incredible strength born of over a decade of community effort.
|
|
190
|
+
|
|
191
|
+
**Dami does not have this.**
|
|
192
|
+
|
|
193
|
+
* **The Dami Trade-off:** Dami prioritizes architectural purity and performance over a vast, plug-and-play ecosystem. It provides a cleaner, more robust foundation for you to build your *own* solutions, free from the constraints and magic of a monolithic framework.
|
|
194
|
+
* **The Bottom Line:** If your project's success depends on quickly integrating a dozen mature, third-party gems for common features, the ActiveRecord ecosystem is currently the more pragmatic choice.
|
|
195
|
+
|
|
196
|
+
#### 2. ...You Need Broad Database Support *Today*.
|
|
197
|
+
|
|
198
|
+
Dami v1.0 is laser-focused on providing a world-class, battle-tested experience for a single database: **SQLite**. This focus allows us to guarantee performance and stability.
|
|
199
|
+
|
|
200
|
+
* **The Dami Trade-off:** We chose to do one thing exceptionally well before expanding. Support for **PostgreSQL** and **MySQL** are high-priority items on the roadmap for the v1.x series, but they are not here today.
|
|
201
|
+
* **The Bottom Line:** If your production environment requires PostgreSQL, MySQL, or another database *right now*, you should wait for a future release of Dami.
|
|
202
|
+
|
|
203
|
+
#### 3. ...Your Team is 100% "Rails Fluent" and on a Tight Deadline.
|
|
204
|
+
|
|
205
|
+
Dami's "Four Pillars" architecture is its greatest strength, but it represents a different paradigm. It requires un-learning the "fat model" habit and embracing an explicit separation of concerns.
|
|
206
|
+
|
|
207
|
+
* **The Dami Trade-off:** Dami offers a long-term solution to the pain points of architectural decay, but this requires an initial investment in learning a new, more disciplined way of organizing code. This shift, while powerful, requires team buy-in.
|
|
208
|
+
* **The Bottom Line:** If your team is under a tight deadline and is composed entirely of senior Rails developers who can move at lightning speed within that ecosystem, the cost of switching paradigms might outweigh the immediate benefits.
|
|
209
|
+
|
|
210
|
+
#### 4. ...You Want an All-in-One, "Convention over Configuration" Framework.
|
|
211
|
+
|
|
212
|
+
Dami is a world-class **ORM and business logic layer**. It is not a full-stack web framework. It has no opinions about routing, controllers, views, or asset pipelines.
|
|
213
|
+
|
|
214
|
+
* **The Dami Position:** This is a feature, not a limitation. Dami is designed to be the powerful data and logic core of *any* Ruby application—a Sinatra or Hanami web app, a command-line tool, a data processing script, or a standalone API. It doesn't lock you into a single way of building.
|
|
215
|
+
* **The Bottom Line:** If you want a single command to generate a full CRUD application from front to back, you are looking for a full-stack framework like Rails, not a specialized, best-in-class component like Dami.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Your Call to Action
|
|
220
|
+
|
|
221
|
+
The Dami Way is a commitment to building better software. It's a choice to favor clarity over magic, purity over convenience, and long-term maintainability over short-term shortcuts.
|
|
222
|
+
|
|
223
|
+
You've seen the difference. You've seen the architecture. Now it's time to decide if you're ready to build on a foundation designed for the long haul.
|
|
224
|
+
|
|
225
|
+
Dami is not a Swiss Army knife. **It's a scalpel. And for the right job, it's the best tool there is.**
|
|
226
|
+
|
|
227
|
+
**Get hyped. Get building.** Welcome to The Dami Way.
|