yaml_exporter 0.1.0 → 0.2.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 +4 -4
- data/.github/workflows/gem-push.yml +20 -29
- data/.github/workflows/test.yml +27 -0
- data/AGENTS.md +34 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile.lock +106 -0
- data/README.md +772 -37
- data/RELEASING.md +45 -0
- data/Rakefile +14 -0
- data/lib/yaml_exporter/builder.rb +93 -0
- data/lib/yaml_exporter/exporter.rb +139 -0
- data/lib/yaml_exporter/importer.rb +86 -0
- data/lib/yaml_exporter/nodes/attribute.rb +70 -0
- data/lib/yaml_exporter/nodes/many_base.rb +215 -0
- data/lib/yaml_exporter/nodes/many_find_by.rb +70 -0
- data/lib/yaml_exporter/nodes/many_positional.rb +14 -0
- data/lib/yaml_exporter/nodes/many_reference.rb +73 -0
- data/lib/yaml_exporter/nodes/many_through.rb +176 -0
- data/lib/yaml_exporter/nodes/one_owned.rb +96 -0
- data/lib/yaml_exporter/nodes/one_reference.rb +72 -0
- data/lib/yaml_exporter/nodes/one_reference_of.rb +128 -0
- data/lib/yaml_exporter/schema.rb +24 -0
- data/lib/yaml_exporter/structure.rb +25 -0
- data/lib/yaml_exporter/type_inference.rb +64 -0
- data/lib/yaml_exporter/version.rb +5 -0
- data/lib/yaml_exporter.rb +53 -8
- data/script/demo_quiz.rb +78 -0
- data/yaml_exporter.gemspec +8 -9
- metadata +58 -13
- data/lib/yaml_serializable/structure_builder.rb +0 -30
- data/lib/yaml_serializable/version.rb +0 -3
- data/lib/yaml_serializable/yaml_exporter.rb +0 -196
data/README.md
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
# YamlExporter
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
with JSON schema generation for validation.
|
|
3
|
+
**Manage ActiveRecord data in YAML files, track it in Git.**
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
YamlExporter serializes a record — and everything it owns — to a human-readable YAML file, and imports the file back in a single transaction. A three-method DSL (`attributes`, `one`, `many`) declares the mapping; `yaml_export` and `yaml_import` round-trip the data.
|
|
7
6
|
|
|
8
|
-
-
|
|
9
|
-
- Deserialize YAML back to ActiveRecord models
|
|
10
|
-
- Generate JSON schemas for model validation
|
|
11
|
-
- Support for nested associations (has_many and has_one)
|
|
12
|
-
- Automatic type inference based on database column types
|
|
7
|
+
Useful whenever a record is really *content*: seed data, CMS-style entries, training material, quiz banks, fixtures — anywhere you'd rather review changes in a pull request than in the database.
|
|
13
8
|
|
|
14
9
|
## Installation
|
|
15
10
|
|
|
@@ -25,70 +20,810 @@ And then execute:
|
|
|
25
20
|
$ bundle install
|
|
26
21
|
```
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+
or install it yourself as:
|
|
29
24
|
|
|
30
25
|
```
|
|
31
26
|
$ gem install yaml_exporter
|
|
32
27
|
```
|
|
33
28
|
|
|
34
|
-
##
|
|
29
|
+
## Hello Yaml
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
Imagine you are a bookstore and want to manage your books in Git.
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
Here is our model:
|
|
34
|
+
|
|
35
|
+
```mermaid
|
|
36
|
+
classDiagram
|
|
37
|
+
class Book {
|
|
38
|
+
- String title
|
|
39
|
+
- String author
|
|
40
|
+
- Decimal price
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Why not create such a file here:
|
|
45
|
+
|
|
46
|
+
`ruby-on-rails.yaml`
|
|
47
|
+
```yaml
|
|
48
|
+
title: Ruby on Rails Tutorial
|
|
49
|
+
author: Michael Hartl
|
|
50
|
+
price: 100
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then, you can configure your ActiveRecord model to be able to export and import this data from and to this file:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
class Book < ActiveRecord::Base
|
|
57
|
+
include YamlExporter
|
|
58
|
+
|
|
59
|
+
yaml_structure do
|
|
60
|
+
attributes :title, :author, :price
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Now you can simply import the data from the file into your ActiveRecord model:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
book = Book.new
|
|
69
|
+
yaml_string = File.read('ruby-on-rails.yaml')
|
|
70
|
+
book.yaml_import(yaml_string)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
And of course you can export the data from your ActiveRecord model to the file:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
yaml_string = book.yaml_export
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Mental model
|
|
80
|
+
|
|
81
|
+
The whole DSL fits in three methods: `attributes`, `one`, and `many`. Everything else is governed by two orthogonal axes and one lifecycle rule.
|
|
82
|
+
|
|
83
|
+
**A YAML file describes one record and everything it owns.** References (via `find_by`) are how you reach outside that ownership boundary to point at records that live in other trees — other files, or records managed elsewhere in code.
|
|
84
|
+
|
|
85
|
+
### Axis 1 — cardinality: pick the method
|
|
86
|
+
|
|
87
|
+
| Method | What it describes |
|
|
88
|
+
| ------------------------- | ----------------------------------- |
|
|
89
|
+
| `attributes :col1, :col2` | Plain columns on the current record |
|
|
90
|
+
| `one :relation, …` | Exactly one related record |
|
|
91
|
+
| `many :relations, …` | A list of related records |
|
|
92
|
+
|
|
93
|
+
### Axis 2 — ownership: block vs. `find_by`
|
|
94
|
+
|
|
95
|
+
The arguments you pass determine both the YAML shape and who owns the record:
|
|
96
|
+
|
|
97
|
+
| Pattern | YAML shape | Ownership |
|
|
98
|
+
| ------------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| **block** | hash | **You own it.** YamlExporter creates, updates, and destroys records to match the YAML. |
|
|
100
|
+
| **`find_by:` only** (no block) | bare string | **Someone else owns it.** YamlExporter only resolves the reference — it never creates or destroys referenced records. |
|
|
101
|
+
| **block + `find_by:`** | hash containing the `find_by` key | You own it; identity is a stable column rather than position (for `many`). |
|
|
102
|
+
|
|
103
|
+
### The combination table
|
|
104
|
+
|
|
105
|
+
| DSL call | YAML shape |
|
|
106
|
+
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| `attributes :title, :price` | `title: …`, `price: …` |
|
|
108
|
+
| `one :book_detail do … end` | nested hash |
|
|
109
|
+
| `one :publisher, find_by: :slug` | bare string: `publisher: some-slug` |
|
|
110
|
+
| `one :responsible_editor, find_by: :slug, of: :user` | bare string: `responsible_editor: some-slug` (resolved via a nested association on the target) |
|
|
111
|
+
| `many :book_parts do … end` | list of hashes, matched by order |
|
|
112
|
+
| `many :book_parts, find_by: :slug do … end` | list of hashes, matched by `slug` |
|
|
113
|
+
| `many :book_parts, find_by: :slug, positioned_by: :position do … end` | list of hashes, matched by `slug`; no `position:` key — the column is derived from the array index |
|
|
114
|
+
| `many :authors, find_by: :slug` | list of bare strings |
|
|
115
|
+
| `many :reviewers, through: :book_reviewers, find_by: :slug` | list of bare strings (join rows managed for you; add `positioned_by:` to derive a join column from order) |
|
|
116
|
+
| `many :reviewers, through: :book_reviewers, find_by: :slug do … end` | list of hashes describing join-model attributes |
|
|
117
|
+
|
|
118
|
+
### Lifecycle rules
|
|
119
|
+
|
|
120
|
+
These apply uniformly to every `yaml_import`:
|
|
121
|
+
|
|
122
|
+
1. **Missing key = `null` = clear it.** Absent attributes are reset to `nil`; absent owned records are destroyed. To tell the importer to ignore a field, omit it from `yaml_structure` — don't leave it blank in the YAML.
|
|
123
|
+
2. **One import = one transaction.** If anything raises, the whole import rolls back and the record is left untouched.
|
|
124
|
+
3. **Validation failures raise.** All writes go through `save!`, so `ActiveRecord::RecordInvalid` aborts the import. Unresolvable references raise `ActiveRecord::RecordNotFound`. The library deliberately never swallows these.
|
|
125
|
+
|
|
126
|
+
## Working with relations
|
|
127
|
+
|
|
128
|
+
The examples below walk through each row of the combination table above, using the same bookstore domain.
|
|
129
|
+
|
|
130
|
+
### `many` with a block (positional)
|
|
131
|
+
|
|
132
|
+
Your book consists of multiple parts and on the website you want to display a bit more information about each part:
|
|
133
|
+
|
|
134
|
+
```mermaid
|
|
135
|
+
classDiagram
|
|
136
|
+
class Book {
|
|
137
|
+
- String title
|
|
138
|
+
- String author
|
|
139
|
+
- Decimal price
|
|
140
|
+
}
|
|
141
|
+
class BookPart {
|
|
142
|
+
- String title
|
|
143
|
+
- String content
|
|
144
|
+
- Integer position
|
|
145
|
+
}
|
|
146
|
+
Book "1" -- "*" BookPart
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Of course, you could create a separate yaml file for each book part and pass the book id, but actually it would be nicer to configure the parts in the book yaml file:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
title: Ruby on Rails Tutorial
|
|
153
|
+
author: Michael Hartl
|
|
154
|
+
price: 100
|
|
155
|
+
book_parts:
|
|
156
|
+
- title: Chapter 1
|
|
157
|
+
content: |-
|
|
158
|
+
This is the first chapter of the book
|
|
159
|
+
position: 1
|
|
160
|
+
- title: Chapter 2
|
|
161
|
+
content: |-
|
|
162
|
+
This is the second chapter of the book
|
|
163
|
+
position: 2
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
To do so, configure your book model to have a `has_many` relation to book parts:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class Book < ActiveRecord::Base
|
|
170
|
+
has_many :book_parts, dependent: :destroy
|
|
171
|
+
|
|
172
|
+
include YamlExporter
|
|
173
|
+
|
|
174
|
+
yaml_structure do
|
|
175
|
+
attributes :title, :author, :price
|
|
176
|
+
many :book_parts do
|
|
177
|
+
attributes :title, :content, :position
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Edge cases**:
|
|
184
|
+
* If there are missing book parts in the database, they will be created.
|
|
185
|
+
* If the database has more book parts than in the yaml file, the extra book parts will be deleted.
|
|
186
|
+
* Existing rows are matched by array index — so re-ordering the list silently overwrites each row with another row's data.
|
|
187
|
+
|
|
188
|
+
Be aware that this way you need to be very careful with the order of the book parts in the yaml file. To avoid this, you can use the `find_by` option:
|
|
189
|
+
|
|
190
|
+
### `many` with `find_by` and a block
|
|
191
|
+
|
|
192
|
+
Let us now identify the book parts by a stable column, for example, let's add a `slug` column to the book parts:
|
|
193
|
+
|
|
194
|
+
```mermaid
|
|
195
|
+
classDiagram
|
|
196
|
+
class Book {
|
|
197
|
+
- String title
|
|
198
|
+
- String author
|
|
199
|
+
- Decimal price
|
|
200
|
+
}
|
|
201
|
+
class BookPart {
|
|
202
|
+
- String title
|
|
203
|
+
- String content
|
|
204
|
+
- Integer position
|
|
205
|
+
- String slug
|
|
206
|
+
}
|
|
207
|
+
Book "1" -- "*" BookPart
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Now you can configure your book model to identify the book parts by the `slug` column:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
class Book < ActiveRecord::Base
|
|
214
|
+
has_many :book_parts, dependent: :destroy
|
|
215
|
+
|
|
216
|
+
include YamlExporter
|
|
217
|
+
|
|
218
|
+
yaml_structure do
|
|
219
|
+
attributes :title, :author, :price
|
|
220
|
+
many :book_parts, find_by: :slug do
|
|
221
|
+
attributes :title, :content, :position
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The corresponding yaml file would look like this:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
title: Ruby on Rails Tutorial
|
|
231
|
+
author: Michael Hartl
|
|
232
|
+
price: 100
|
|
233
|
+
book_parts:
|
|
234
|
+
- slug: chapter-1
|
|
235
|
+
title: Chapter 1
|
|
236
|
+
content: |-
|
|
237
|
+
This is the first chapter of the book
|
|
238
|
+
position: 1
|
|
239
|
+
- slug: chapter-2
|
|
240
|
+
title: Chapter 2
|
|
241
|
+
content: |-
|
|
242
|
+
This is the second chapter of the book
|
|
243
|
+
position: 2
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Identification**: YamlExporter uses `book.book_parts.find_by(slug: "chapter-1")` to identify each book part. Slugs do not need to be unique across all books; and if multiple book parts for the same book share a slug, only the first is matched and the rest are deleted.
|
|
247
|
+
|
|
248
|
+
**Edge cases**:
|
|
249
|
+
* Missing book parts in the database will be created.
|
|
250
|
+
* Extra book parts in the database (absent from the yaml) will be deleted.
|
|
251
|
+
* The order of the book parts in the yaml file doesn't matter for *identity* — but `position: 1`/`position: 2` is still being managed by hand.
|
|
252
|
+
|
|
253
|
+
The next section shows how to make the YAML order itself drive the `position` column.
|
|
254
|
+
|
|
255
|
+
### `many` with `positioned_by:` (derived position)
|
|
256
|
+
|
|
257
|
+
The previous example stores `position` as a regular column and requires the user to keep the numbers in sync with the list order. That's both tedious and a constant source of merge conflicts.
|
|
258
|
+
|
|
259
|
+
`positioned_by:` hands that column to YamlExporter: on import, each entry's position is set from its 1-based array index; on export, the list is sorted by the position column ASC and the column is omitted from the YAML (the order of the list **is** the position).
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
class Book < ActiveRecord::Base
|
|
263
|
+
has_many :book_parts, dependent: :destroy
|
|
264
|
+
|
|
265
|
+
include YamlExporter
|
|
266
|
+
|
|
267
|
+
yaml_structure do
|
|
268
|
+
attributes :title, :author, :price
|
|
269
|
+
many :book_parts, find_by: :slug, positioned_by: :position do
|
|
270
|
+
attributes :title, :content
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The YAML gets tidier — no `position:` keys inside the entries:
|
|
277
|
+
|
|
278
|
+
```yaml
|
|
279
|
+
title: Ruby on Rails Tutorial
|
|
280
|
+
author: Michael Hartl
|
|
281
|
+
price: 100
|
|
282
|
+
book_parts:
|
|
283
|
+
- slug: chapter-1
|
|
284
|
+
title: Chapter 1
|
|
285
|
+
content: |-
|
|
286
|
+
This is the first chapter of the book
|
|
287
|
+
- slug: chapter-2
|
|
288
|
+
title: Chapter 2
|
|
289
|
+
content: |-
|
|
290
|
+
This is the second chapter of the book
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Reordering is now a single-line move in the YAML file, and the `position` column in the database tracks it automatically.
|
|
294
|
+
|
|
295
|
+
**Edge cases**:
|
|
296
|
+
* The `positioned_by:` column may **not** also appear in the block's `attributes` list — the DSL owns it for the owned record. Declaring both raises at load time.
|
|
297
|
+
* A `position:` key (or whatever column you named) inside a YAML entry is rejected on import, for the same reason.
|
|
298
|
+
* If the database has gaps, duplicates, or `NULL`s in the position column from earlier code paths, a full import rewrites them 1..N cleanly. The YAML is always the source of truth.
|
|
299
|
+
* `positioned_by:` is available wherever there is an owned record to write the column onto — so with or without `find_by:`, and also in combination with `through:` (where the column lives on the join model, same as other block attributes). It is **not** available on `many :authors, find_by: :slug` (no block), since there is no owned record.
|
|
300
|
+
|
|
301
|
+
### `many` with `find_by` and no block (reference list)
|
|
302
|
+
|
|
303
|
+
Sometimes the children of a `has_many` are not owned by the parent at all — they are managed elsewhere (in their own yaml files, or by the user directly), and the parent only needs to reference them. The prime example is a many-to-many relation. Say authors are their own records and a book just points at them:
|
|
304
|
+
|
|
305
|
+
```mermaid
|
|
306
|
+
classDiagram
|
|
307
|
+
class Book {
|
|
308
|
+
- String title
|
|
309
|
+
- Decimal price
|
|
310
|
+
}
|
|
311
|
+
class Author {
|
|
312
|
+
- String name
|
|
313
|
+
- String slug
|
|
314
|
+
}
|
|
315
|
+
Book "*" -- "*" Author
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Since `has_and_belongs_to_many` has no join-model attributes to manage, it fits this pattern exactly — so the example below uses HABTM to demonstrate. Drop the block entirely and use `find_by` to declare the key:
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
class Book < ActiveRecord::Base
|
|
322
|
+
has_and_belongs_to_many :authors
|
|
323
|
+
|
|
324
|
+
include YamlExporter
|
|
325
|
+
|
|
326
|
+
yaml_structure do
|
|
327
|
+
attributes :title, :price
|
|
328
|
+
many :authors, find_by: :slug
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Because the block is missing, each author appears in the YAML as a bare string (the slug), not a hash:
|
|
334
|
+
|
|
335
|
+
```yaml
|
|
336
|
+
title: Ruby on Rails Tutorial
|
|
337
|
+
price: 38
|
|
338
|
+
authors:
|
|
339
|
+
- michael-hartl
|
|
340
|
+
- another-author
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
This same flavor works for `has_and_belongs_to_many` (as above) and for any `has_many` where the parent shouldn't manage the children's attributes.
|
|
344
|
+
|
|
345
|
+
**Identification**: YamlExporter uses `Author.find_by(slug: "michael-hartl")` to resolve each entry.
|
|
346
|
+
|
|
347
|
+
**Edge cases**:
|
|
348
|
+
* If a referenced author does not exist in the database, `ActiveRecord::RecordNotFound` is raised — the library never auto-creates referenced records.
|
|
349
|
+
* If the database has more associations than the yaml file, the extra associations are removed — for HABTM only the join rows are removed, the referenced records themselves are left untouched.
|
|
350
|
+
* The order of the entries in the yaml file doesn't matter.
|
|
351
|
+
|
|
352
|
+
### `many` with `through:` (has_many :through with join attributes)
|
|
353
|
+
|
|
354
|
+
`has_many :through` is not a separate DSL method — how you expose it depends on whether the join model carries its own attributes.
|
|
355
|
+
|
|
356
|
+
**If the join has no extra attributes** drop the block entirely — `many :reviewers, through: :book_reviewers, find_by: :slug` is a bare reference list, identical in shape to `many :authors, find_by: :slug` (a flat list of `find_by` values). The distinction is the *presence* of a block, not its contents: omit it for bare strings; pass one (even an empty one) to opt into hash-shaped entries. You don't have to expose the join association separately; YamlExporter still creates and destroys the join rows for you, it just doesn't ask the YAML for any join attributes:
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
yaml_structure do
|
|
360
|
+
attributes :title, :price
|
|
361
|
+
many :reviewers, through: :book_reviewers, find_by: :slug
|
|
362
|
+
end
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
```yaml
|
|
366
|
+
title: Ruby on Rails Tutorial
|
|
367
|
+
price: 100
|
|
368
|
+
reviewers:
|
|
369
|
+
- alice
|
|
370
|
+
- bob
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Add `positioned_by:` when the join carries an order column but nothing else — the column is derived from the YAML order, so the list stays a flat list of strings:
|
|
39
374
|
|
|
40
375
|
```ruby
|
|
376
|
+
many :reviewers, through: :book_reviewers, find_by: :slug, positioned_by: :position
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**If the join carries its own attributes** (e.g. `finished` on a `book_reviewers` join), declare `many` on the `:through` target and pass both `through:` and `find_by:`. The block then describes attributes of the **join model**:
|
|
380
|
+
|
|
381
|
+
```mermaid
|
|
382
|
+
classDiagram
|
|
383
|
+
class Book {
|
|
384
|
+
- String title
|
|
385
|
+
- Decimal price
|
|
386
|
+
}
|
|
387
|
+
class BookReviewer {
|
|
388
|
+
- String slug
|
|
389
|
+
- Boolean finished
|
|
390
|
+
}
|
|
391
|
+
class Reviewer {
|
|
392
|
+
- String name
|
|
393
|
+
- String slug
|
|
394
|
+
}
|
|
395
|
+
Book "*" -- "*" BookReviewer
|
|
396
|
+
BookReviewer "*" -- "1" Reviewer
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
class Book < ActiveRecord::Base
|
|
401
|
+
has_many :book_reviewers, dependent: :destroy
|
|
402
|
+
has_many :reviewers, through: :book_reviewers
|
|
41
403
|
|
|
42
|
-
class Quiz < ApplicationRecord
|
|
43
404
|
include YamlExporter
|
|
44
405
|
|
|
45
406
|
yaml_structure do
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
yaml_has_many :answers do
|
|
50
|
-
yaml_attribute :text, :is_correct, :impact
|
|
51
|
-
end
|
|
407
|
+
attributes :title, :price
|
|
408
|
+
many :reviewers, through: :book_reviewers, find_by: :slug do
|
|
409
|
+
attributes :finished # lives on book_reviewers, the join model
|
|
52
410
|
end
|
|
53
411
|
end
|
|
54
412
|
end
|
|
55
413
|
```
|
|
56
414
|
|
|
57
|
-
|
|
415
|
+
```yaml
|
|
416
|
+
title: Ruby on Rails Tutorial
|
|
417
|
+
price: 100
|
|
418
|
+
reviewers:
|
|
419
|
+
- slug: alice
|
|
420
|
+
finished: true
|
|
421
|
+
- slug: bob
|
|
422
|
+
finished: false
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
On import, each entry is processed like this:
|
|
58
426
|
|
|
59
|
-
|
|
427
|
+
1. Find the `Reviewer` by slug. (`Reviewer.find_by(slug: "alice")`)
|
|
428
|
+
2. Find-or-build the `book_reviewer` join row linking this book and that reviewer — the identity of the join is `(book_id, reviewer_id)`, so no positional matching is needed. (`book.book_reviewers.find_or_create_by(reviewer_id: reviewer.id)`)
|
|
429
|
+
3. Apply the block's attributes (`finished`) to the join row.
|
|
430
|
+
4. Destroy any join rows whose reviewer is no longer listed.
|
|
431
|
+
|
|
432
|
+
`positioned_by:` works here too — the column lives on the join model, so e.g. `many :reviewers, through: :book_reviewers, find_by: :slug, positioned_by: :position do … end` makes the YAML order drive `book_reviewers.position`.
|
|
433
|
+
|
|
434
|
+
The alternative — declaring `many` directly on the **join model itself** — is also supported and useful when you want to expose the join row as its own first-class concept rather than hiding it behind the target association.
|
|
435
|
+
|
|
436
|
+
### `one` with a block (owned)
|
|
437
|
+
|
|
438
|
+
This is the `has_one` pattern. Suppose every book has exactly one `book_detail` containing extra information like a summary and publication year:
|
|
439
|
+
|
|
440
|
+
```mermaid
|
|
441
|
+
classDiagram
|
|
442
|
+
class Book {
|
|
443
|
+
- String title
|
|
444
|
+
- String author
|
|
445
|
+
- Decimal price
|
|
446
|
+
}
|
|
447
|
+
class BookDetail {
|
|
448
|
+
- String summary
|
|
449
|
+
- Integer publication_year
|
|
450
|
+
}
|
|
451
|
+
Book "1" -- "1" BookDetail
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Let's configure your models for this:
|
|
60
455
|
|
|
61
456
|
```ruby
|
|
62
|
-
|
|
63
|
-
|
|
457
|
+
class Book < ActiveRecord::Base
|
|
458
|
+
has_one :book_detail, dependent: :destroy
|
|
459
|
+
|
|
460
|
+
include YamlExporter
|
|
461
|
+
|
|
462
|
+
yaml_structure do
|
|
463
|
+
attributes :title, :author, :price
|
|
464
|
+
one :book_detail do
|
|
465
|
+
attributes :summary, :publication_year
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
class BookDetail < ActiveRecord::Base
|
|
471
|
+
belongs_to :book
|
|
472
|
+
end
|
|
64
473
|
```
|
|
65
474
|
|
|
66
|
-
|
|
475
|
+
Your YAML file would look like:
|
|
476
|
+
|
|
477
|
+
```yaml
|
|
478
|
+
title: Ruby on Rails Tutorial
|
|
479
|
+
author: Michael Hartl
|
|
480
|
+
price: 100
|
|
481
|
+
book_detail:
|
|
482
|
+
summary: |-
|
|
483
|
+
A practical introduction to Ruby on Rails development.
|
|
484
|
+
publication_year: 2022
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Identification**: YamlExporter uses `book.book_detail` to follow the association. There can only be one book detail per book.
|
|
488
|
+
|
|
489
|
+
**Edge cases**:
|
|
490
|
+
* If there is no book detail in the database, it will be created.
|
|
491
|
+
* If the `book_detail:` key is missing from the YAML or set to `null`, the existing book detail is destroyed — omission and `null` are treated the same. If that violates a model-level validation (e.g. `book_detail` is required), the resulting `ActiveRecord` error propagates and the whole import is rolled back.
|
|
492
|
+
|
|
493
|
+
### `one` with `find_by` (reference)
|
|
494
|
+
|
|
495
|
+
This is the `belongs_to` pattern. A book is published by a publisher — the book has the `publisher_id` foreign key, and the publisher is a first-class record managed in its own YAML file elsewhere.
|
|
496
|
+
|
|
497
|
+
```mermaid
|
|
498
|
+
classDiagram
|
|
499
|
+
class Book {
|
|
500
|
+
- String title
|
|
501
|
+
- String author
|
|
502
|
+
- Decimal price
|
|
503
|
+
}
|
|
504
|
+
class Publisher {
|
|
505
|
+
- String name
|
|
506
|
+
- String slug
|
|
507
|
+
}
|
|
508
|
+
Book "*" -- "1" Publisher
|
|
509
|
+
```
|
|
67
510
|
|
|
68
|
-
|
|
511
|
+
We could dump the `publisher_id` column to the book yaml, but that's brittle and not human-friendly. Instead we reference the publisher by a stable column:
|
|
69
512
|
|
|
70
513
|
```ruby
|
|
71
|
-
|
|
72
|
-
|
|
514
|
+
class Book < ActiveRecord::Base
|
|
515
|
+
belongs_to :publisher
|
|
516
|
+
|
|
517
|
+
include YamlExporter
|
|
518
|
+
|
|
519
|
+
yaml_structure do
|
|
520
|
+
attributes :title, :author, :price
|
|
521
|
+
one :publisher, find_by: :slug
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Our yaml file now looks like this:
|
|
527
|
+
|
|
528
|
+
```yaml
|
|
529
|
+
title: Ruby on Rails Tutorial
|
|
530
|
+
author: Michael Hartl
|
|
531
|
+
price: 100
|
|
532
|
+
publisher: addison-wesley
|
|
73
533
|
```
|
|
74
534
|
|
|
75
|
-
|
|
535
|
+
**Identification**: YamlExporter uses `Publisher.find_by(slug: "addison-wesley")` to resolve the publisher. If several publishers share the slug, only the first match is used.
|
|
536
|
+
|
|
537
|
+
**Edge cases**:
|
|
538
|
+
* If no publisher matches the slug in the database, `ActiveRecord::RecordNotFound` is raised.
|
|
539
|
+
|
|
540
|
+
**Two restrictions apply**, both following from the ownership model:
|
|
541
|
+
|
|
542
|
+
* `find_by` is **required** for a reference-flavored `one`. Without it, the only alternative would be exposing the raw foreign key (`publisher_id`) in the YAML using the `attributes` method.
|
|
543
|
+
* A block is **not allowed** when `find_by` is given. The publisher owns itself — a book is just one of many records pointing at it — so the book's YAML has no business defining the publisher's attributes. Manage the publisher from its own YAML file instead.
|
|
544
|
+
|
|
545
|
+
### `one` with `find_by` and `of:` (indirect reference)
|
|
546
|
+
|
|
547
|
+
Sometimes the record you point at does not carry the identifying column directly — a companion record does. A classic case is a two-level identity hierarchy:
|
|
548
|
+
|
|
549
|
+
```mermaid
|
|
550
|
+
classDiagram
|
|
551
|
+
class User {
|
|
552
|
+
- String name
|
|
553
|
+
- String slug
|
|
554
|
+
}
|
|
555
|
+
class CorporateUser {
|
|
556
|
+
- String name
|
|
557
|
+
}
|
|
558
|
+
class Book {
|
|
559
|
+
- String title
|
|
560
|
+
}
|
|
561
|
+
CorporateUser "*" -- "1" User
|
|
562
|
+
Book "*" -- "0..1" CorporateUser : responsible_editor
|
|
563
|
+
```
|
|
76
564
|
|
|
77
|
-
|
|
565
|
+
`Book belongs_to :responsible_editor` (a `CorporateUser`), but `CorporateUser` has no slug of its own. The slug lives on the associated `User`. Storing an opaque database id in the YAML is brittle; referencing the user's slug is human-readable and stable.
|
|
566
|
+
|
|
567
|
+
The `of:` keyword lets you do exactly that:
|
|
78
568
|
|
|
79
569
|
```ruby
|
|
80
|
-
|
|
570
|
+
class Book < ActiveRecord::Base
|
|
571
|
+
belongs_to :responsible_editor, class_name: 'CorporateUser',
|
|
572
|
+
foreign_key: :responsible_editor_id, optional: true
|
|
573
|
+
|
|
574
|
+
include YamlExporter
|
|
575
|
+
|
|
576
|
+
yaml_structure do
|
|
577
|
+
attributes :title
|
|
578
|
+
one :responsible_editor, find_by: :slug, of: :user
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
class CorporateUser < ActiveRecord::Base
|
|
583
|
+
belongs_to :user
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
class User < ActiveRecord::Base
|
|
587
|
+
has_one :corporate_user
|
|
588
|
+
end
|
|
81
589
|
```
|
|
82
590
|
|
|
83
|
-
|
|
591
|
+
The YAML stores the user's slug:
|
|
592
|
+
|
|
593
|
+
```yaml
|
|
594
|
+
title: Ruby on Rails Tutorial
|
|
595
|
+
responsible_editor: alice
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
On **import**, YamlExporter:
|
|
599
|
+
1. Looks up `User.find_by(slug: "alice")`.
|
|
600
|
+
2. Navigates back to `CorporateUser` by reversing the FK (`CorporateUser.find_by(user_id: alice.id)`).
|
|
601
|
+
3. Assigns the result to `book.responsible_editor`.
|
|
602
|
+
|
|
603
|
+
On **export**, `book.responsible_editor.user.slug` is emitted.
|
|
84
604
|
|
|
85
|
-
|
|
86
|
-
objects in the generated schema.
|
|
605
|
+
**Restrictions**:
|
|
87
606
|
|
|
88
|
-
|
|
607
|
+
* `of:` requires `find_by:` — the two always appear together.
|
|
608
|
+
* A block is not allowed when `of:` is used (same ownership rule as plain `find_by:`).
|
|
609
|
+
* The `of:` association must be a **1:[0,1]** relation (`belongs_to` or `has_one`). Using a `has_many` association raises at class-load time.
|
|
610
|
+
* If the `of:` record exists but no target record is linked to it (e.g. a `User` with no `CorporateUser`), `ActiveRecord::RecordNotFound` is raised on import.
|
|
89
611
|
|
|
90
|
-
|
|
612
|
+
## Putting it all together
|
|
91
613
|
|
|
92
|
-
|
|
614
|
+
Let us now put all together:
|
|
93
615
|
|
|
94
|
-
|
|
616
|
+
```ruby
|
|
617
|
+
class Book < ActiveRecord::Base
|
|
618
|
+
has_many :book_parts, dependent: :destroy
|
|
619
|
+
has_one :book_detail, dependent: :destroy
|
|
620
|
+
has_and_belongs_to_many :authors
|
|
621
|
+
belongs_to :publisher
|
|
622
|
+
has_many :book_reviewers, dependent: :destroy
|
|
623
|
+
has_many :reviewers, through: :book_reviewers
|
|
624
|
+
belongs_to :responsible_editor, class_name: 'CorporateUser',
|
|
625
|
+
foreign_key: :responsible_editor_id, optional: true
|
|
626
|
+
|
|
627
|
+
include YamlExporter
|
|
628
|
+
|
|
629
|
+
yaml_structure do
|
|
630
|
+
attributes :title, :price
|
|
631
|
+
many :book_parts, find_by: :slug, positioned_by: :position do
|
|
632
|
+
attributes :title, :content
|
|
633
|
+
end
|
|
634
|
+
one :book_detail do
|
|
635
|
+
attributes :summary, :publication_year
|
|
636
|
+
end
|
|
637
|
+
many :authors, find_by: :slug
|
|
638
|
+
one :publisher, find_by: :slug
|
|
639
|
+
many :reviewers, through: :book_reviewers, find_by: :slug do
|
|
640
|
+
attributes :finished
|
|
641
|
+
end
|
|
642
|
+
one :responsible_editor, find_by: :slug, of: :user
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
class CorporateUser < ActiveRecord::Base
|
|
647
|
+
belongs_to :user
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
class User < ActiveRecord::Base
|
|
651
|
+
has_one :corporate_user
|
|
652
|
+
end
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Our yaml file now looks like this:
|
|
656
|
+
|
|
657
|
+
```yaml
|
|
658
|
+
title: Ruby on Rails Tutorial
|
|
659
|
+
price: 100
|
|
660
|
+
book_parts:
|
|
661
|
+
- slug: chapter-1
|
|
662
|
+
title: Chapter 1
|
|
663
|
+
content: |-
|
|
664
|
+
This is the first chapter of the book
|
|
665
|
+
- slug: chapter-2
|
|
666
|
+
title: Chapter 2
|
|
667
|
+
content: |-
|
|
668
|
+
This is the second chapter of the book
|
|
669
|
+
book_detail:
|
|
670
|
+
summary: |-
|
|
671
|
+
A practical introduction to Ruby on Rails development.
|
|
672
|
+
publication_year: 2022
|
|
673
|
+
authors:
|
|
674
|
+
- michael-hartl
|
|
675
|
+
- another-author
|
|
676
|
+
publisher: addison-wesley
|
|
677
|
+
reviewers:
|
|
678
|
+
- slug: alice
|
|
679
|
+
finished: true
|
|
680
|
+
- slug: bob
|
|
681
|
+
finished: false
|
|
682
|
+
responsible_editor: alice
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
And the resulting object graph:
|
|
686
|
+
|
|
687
|
+
```mermaid
|
|
688
|
+
classDiagram
|
|
689
|
+
class Book {
|
|
690
|
+
- String title
|
|
691
|
+
- Decimal price
|
|
692
|
+
}
|
|
693
|
+
class BookPart {
|
|
694
|
+
- String slug
|
|
695
|
+
- String title
|
|
696
|
+
- String content
|
|
697
|
+
- Integer position
|
|
698
|
+
}
|
|
699
|
+
class BookDetail {
|
|
700
|
+
- String summary
|
|
701
|
+
- Integer publication_year
|
|
702
|
+
}
|
|
703
|
+
class Author {
|
|
704
|
+
- String name
|
|
705
|
+
- String slug
|
|
706
|
+
}
|
|
707
|
+
class Publisher {
|
|
708
|
+
- String name
|
|
709
|
+
- String slug
|
|
710
|
+
}
|
|
711
|
+
class BookReviewer {
|
|
712
|
+
- Boolean finished
|
|
713
|
+
}
|
|
714
|
+
class Reviewer {
|
|
715
|
+
- String name
|
|
716
|
+
- String slug
|
|
717
|
+
}
|
|
718
|
+
class CorporateUser {
|
|
719
|
+
- String name
|
|
720
|
+
}
|
|
721
|
+
class User {
|
|
722
|
+
- String name
|
|
723
|
+
- String slug
|
|
724
|
+
}
|
|
725
|
+
Book "1" -- "*" BookPart
|
|
726
|
+
Book "1" -- "1" BookDetail
|
|
727
|
+
Book "*" -- "*" Author
|
|
728
|
+
Book "*" -- "1" Publisher
|
|
729
|
+
Book "1" -- "*" BookReviewer
|
|
730
|
+
BookReviewer "*" -- "1" Reviewer
|
|
731
|
+
Book "*" -- "0..1" CorporateUser : responsible_editor
|
|
732
|
+
CorporateUser "*" -- "1" User
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
## Behaviors of `yaml_import` and `yaml_export`
|
|
736
|
+
|
|
737
|
+
`yaml_import` and `yaml_export` are instance methods included in the model. So if you want to import a model from a yaml file, you first find or create an object and then call `yaml_import` on it:
|
|
738
|
+
|
|
739
|
+
```ruby
|
|
740
|
+
book = Book.new
|
|
741
|
+
book.yaml_import(File.read('ruby-on-rails.yaml'))
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
And of course you can export the data from your ActiveRecord model to the file:
|
|
745
|
+
|
|
746
|
+
```ruby
|
|
747
|
+
yaml_string = book.yaml_export
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
We prefer to identify the objects by the filename of the file and a slug field. So if you have a directory `books` you can import all files in the directory with:
|
|
751
|
+
|
|
752
|
+
```ruby
|
|
753
|
+
Dir.glob('books/*.yaml').each do |file|
|
|
754
|
+
book = Book.find_or_create_by(slug: File.basename(file, '.yaml'))
|
|
755
|
+
book.yaml_import(File.read(file))
|
|
756
|
+
end
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
**Import edge cases** (apply to every `yaml_import`, in addition to the lifecycle rules in the [mental model](#lifecycle-rules)):
|
|
760
|
+
|
|
761
|
+
* Duplicate keys inside the same YAML list — e.g. two `slug: chapter-1` entries under one `book_parts:` — raise an error. Each `find_by` key must be unique within its list.
|
|
762
|
+
* Missing / `null` clears not just plain `attributes` columns, but also `one` references (the foreign key is cleared) and `one` owned children (the child is destroyed).
|
|
763
|
+
|
|
764
|
+
**Export behavior** (mirrors the import rules):
|
|
765
|
+
|
|
766
|
+
* Empty values are omitted by default. A `nil` column, a missing `one` reference, an absent owned `one`, and an empty `many` list are all left out of the document entirely. This is round-trip safe: import treats a missing key, an explicit `null`, and an empty list the same way. To keep explicit `null`s in the file — e.g. so reviewers can discover optional fields — call `yaml_export(omit_nil: false)`. An owned `one` child that exists but has only `nil` attributes is still emitted (as `{}`) — dropping it would destroy the child on re-import.
|
|
767
|
+
* `text` columns are written as YAML literal block scalars (`|`), so multi-line and long-form content stays readable and diff-friendly. `string`/varchar columns stay inline regardless of length, and non-string scalars (numbers, booleans, …) are unaffected. The choice follows the column type, not the value, so a model's files always look the same. Trailing whitespace on a line (and carriage returns) is stripped on export — it would otherwise force YAML to fall back to an inline double-quoted scalar, and it is almost always an accidental typo. Emoji and other astral-plane characters (codepoints ≥ U+10000) are emitted literally and stay in block style too, despite a libyaml quirk that would otherwise escape them into an inline scalar; ordinary accented and umlaut characters were never affected.
|
|
768
|
+
* The output is a plain YAML document, without a leading `---` marker.
|
|
769
|
+
* Lists are written in a stable, diff-friendly order. The rule is: take the first rule that applies, top to bottom:
|
|
770
|
+
1. `many` with `positioned_by:` → sorted by the position column ASC; the column itself is omitted from each entry's hash.
|
|
771
|
+
2. `many` with `find_by:` (with or without a block, including the `through:` variant, without `positioned_by:`) → sorted by the `find_by` column so adding or removing an entry only touches its own line in Git diffs.
|
|
772
|
+
3. `many` with a block only (no `find_by:`) → the collection's SQL order, which is the order the children were inserted (this is also the order positional matching relies on).
|
|
773
|
+
|
|
774
|
+
## API reference
|
|
775
|
+
|
|
776
|
+
All DSL methods are declared inside a `yaml_structure do … end` block on the model. Inside the block, `attributes`, `one` and `many` are resolved against a builder — they never clash with ActiveRecord's own class-level methods (e.g. `attribute`, `has_one`).
|
|
777
|
+
|
|
778
|
+
### `attributes(*names)`
|
|
779
|
+
|
|
780
|
+
Columns of the model that are serialized and deserialized. Missing keys in the YAML reset the corresponding columns to `nil` on import.
|
|
781
|
+
|
|
782
|
+
### `one(name, find_by: nil, of: nil, &block)`
|
|
783
|
+
|
|
784
|
+
A single related record. Exactly one of `find_by:` or a block must be given:
|
|
785
|
+
|
|
786
|
+
| Call | YAML shape | Meaning |
|
|
787
|
+
| --------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
|
|
788
|
+
| `one :child do … end` | nested hash | Owned child (the `has_one` pattern). |
|
|
789
|
+
| `one :child, find_by: :slug` | bare string | Reference to a record managed elsewhere (the `belongs_to` pattern). |
|
|
790
|
+
| `one :child, find_by: :slug, of: :companion` | bare string | Indirect reference: the slug lives on a companion record reachable via the `companion` association on the target. |
|
|
791
|
+
|
|
792
|
+
`of:` requires `find_by:` and cannot be combined with a block. The `of:` association must be a 1:[0,1] relation (`belongs_to` or `has_one`). See [`one` with `find_by` and `of:`](#one-with-find_by-and-of-indirect-reference) for a worked example.
|
|
793
|
+
|
|
794
|
+
Passing both a block and `find_by:` is rejected — see the ownership reasoning in [`one` with `find_by`](#one-with-find_by-reference).
|
|
795
|
+
|
|
796
|
+
### `many(name, positioned_by: nil, find_by: nil, through: nil, &block)`
|
|
797
|
+
|
|
798
|
+
A list of related records. The flavor is picked from the combination of `positioned_by:`, `find_by:`, `through:` and a block:
|
|
799
|
+
|
|
800
|
+
| Call | YAML shape | Meaning |
|
|
801
|
+
| -------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
802
|
+
| `many :children do … end` | list of hashes, matched by order | Children fully managed, identity by position. |
|
|
803
|
+
| `many :children, positioned_by: :position do … end` | same as above, without `position:` in each hash | Like the previous row, but the position column is derived from the 1-based array index (and omitted on export). |
|
|
804
|
+
| `many :children, find_by: :slug do … end` | list of hashes containing the `slug:` key | Children fully managed, identity by a stable column. |
|
|
805
|
+
| `many :children, find_by: :slug, positioned_by: :position do … end` | same as above, without `position:` in each hash | Like the previous row, but the named column is derived from the 1-based array index (and omitted on export). |
|
|
806
|
+
| `many :children, find_by: :slug` | list of bare strings | Children referenced by key, managed elsewhere (HABTM pattern). |
|
|
807
|
+
| `many :children, through: :joins, find_by: :slug do … end` | list of hashes containing the `slug:` key | `has_many :through` where the block describes attributes of the **join model**. |
|
|
808
|
+
| `many :children, through: :joins, find_by: :slug, positioned_by: :position do …` | same as above, without `position:` in each hash | As above, with the position column derived on the **join model**. |
|
|
809
|
+
|
|
810
|
+
`positioned_by:` requires a block – there must be an owned record to write the column onto – and therefore cannot be used with the reference-list flavor of `many`.
|
|
811
|
+
|
|
812
|
+
### Import / export
|
|
813
|
+
|
|
814
|
+
* `instance.yaml_import(yaml_string)` — updates `instance` in place from the YAML, inside a single transaction. Returns the instance.
|
|
815
|
+
* `instance.yaml_export(omit_nil: true)` — returns a YAML string for `instance` following its `yaml_structure`. With `omit_nil: true` (the default) keys whose value is empty — a `nil` attribute/reference, an absent owned child, or an empty `many` list — are left out. Pass `omit_nil: false` to keep them as explicit `null`s, which is handy when you want optional fields to stay visible in the file.
|
|
816
|
+
|
|
817
|
+
Whether a string is written inline or as a literal block scalar (`|`) is decided by the database column type, not by `yaml_export` arguments: `text` columns always use block style, `string`/varchar columns always stay inline. See [Export behavior](#behaviors-of-yaml_import-and-yaml_export).
|
|
818
|
+
|
|
819
|
+
### `ModelClass.yaml_schema`
|
|
820
|
+
|
|
821
|
+
Returns a JSON-schema-like hash describing the YAML shape declared by `yaml_structure`. Useful for generating editor support or validating YAML out-of-band. Invalid YAML passed to `yaml_import` raises with a message pointing at the violated part of the schema.
|
|
822
|
+
|
|
823
|
+
## Development
|
|
824
|
+
|
|
825
|
+
Run the test suite with:
|
|
826
|
+
|
|
827
|
+
```
|
|
828
|
+
$ bundle exec rake test
|
|
829
|
+
```
|