expressir 2.1.30 → 2.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +98 -0
  3. data/.github/workflows/links.yml +100 -0
  4. data/.github/workflows/rake.yml +4 -0
  5. data/.github/workflows/release.yml +5 -0
  6. data/.github/workflows/validate_schemas.yml +1 -1
  7. data/.gitignore +3 -0
  8. data/.rubocop.yml +1 -1
  9. data/.rubocop_todo.yml +244 -39
  10. data/Gemfile +2 -1
  11. data/README.adoc +621 -54
  12. data/docs/Gemfile +12 -0
  13. data/docs/_config.yml +141 -0
  14. data/docs/_guides/changes/changes-format.adoc +778 -0
  15. data/docs/_guides/changes/importing-eengine.adoc +898 -0
  16. data/docs/_guides/changes/index.adoc +396 -0
  17. data/docs/_guides/changes/programmatic-usage.adoc +1038 -0
  18. data/docs/_guides/changes/validating-changes.adoc +681 -0
  19. data/docs/_guides/cli/benchmark-performance.adoc +834 -0
  20. data/docs/_guides/cli/coverage-analysis.adoc +921 -0
  21. data/docs/_guides/cli/format-schemas.adoc +547 -0
  22. data/docs/_guides/cli/index.adoc +8 -0
  23. data/docs/_guides/cli/managing-changes.adoc +927 -0
  24. data/docs/_guides/cli/validate-ascii.adoc +645 -0
  25. data/docs/_guides/cli/validate-schemas.adoc +534 -0
  26. data/docs/_guides/index.adoc +165 -0
  27. data/docs/_guides/ler/creating-packages.adoc +664 -0
  28. data/docs/_guides/ler/index.adoc +305 -0
  29. data/docs/_guides/ler/loading-packages.adoc +707 -0
  30. data/docs/_guides/ler/package-formats.adoc +748 -0
  31. data/docs/_guides/ler/querying-packages.adoc +826 -0
  32. data/docs/_guides/ler/validating-packages.adoc +750 -0
  33. data/docs/_guides/liquid/basic-templates.adoc +813 -0
  34. data/docs/_guides/liquid/documentation-generation.adoc +1042 -0
  35. data/docs/_guides/liquid/drops-reference.adoc +829 -0
  36. data/docs/_guides/liquid/filters-and-tags.adoc +912 -0
  37. data/docs/_guides/liquid/index.adoc +468 -0
  38. data/docs/_guides/manifests/creating-manifests.adoc +483 -0
  39. data/docs/_guides/manifests/index.adoc +307 -0
  40. data/docs/_guides/manifests/resolving-manifests.adoc +557 -0
  41. data/docs/_guides/manifests/validating-manifests.adoc +713 -0
  42. data/docs/_guides/ruby-api/formatting-schemas.adoc +605 -0
  43. data/docs/_guides/ruby-api/index.adoc +257 -0
  44. data/docs/_guides/ruby-api/parsing-files.adoc +421 -0
  45. data/docs/_guides/ruby-api/search-engine.adoc +609 -0
  46. data/docs/_guides/ruby-api/working-with-repository.adoc +577 -0
  47. data/docs/_pages/data-model.adoc +665 -0
  48. data/docs/_pages/express-language.adoc +506 -0
  49. data/docs/_pages/getting-started.adoc +414 -0
  50. data/docs/_pages/index.adoc +116 -0
  51. data/docs/_pages/introduction.adoc +256 -0
  52. data/docs/_pages/ler-packages.adoc +837 -0
  53. data/docs/_pages/parsers.adoc +683 -0
  54. data/docs/_pages/schema-manifests.adoc +431 -0
  55. data/docs/_references/index.adoc +228 -0
  56. data/docs/_tutorials/creating-ler-package.adoc +735 -0
  57. data/docs/_tutorials/documentation-coverage.adoc +795 -0
  58. data/docs/_tutorials/index.adoc +221 -0
  59. data/docs/_tutorials/liquid-templates.adoc +806 -0
  60. data/docs/_tutorials/parsing-your-first-schema.adoc +522 -0
  61. data/docs/_tutorials/querying-schemas.adoc +751 -0
  62. data/docs/_tutorials/working-with-multiple-schemas.adoc +676 -0
  63. data/docs/index.adoc +242 -0
  64. data/docs/lychee.toml +84 -0
  65. data/examples/demo_ler_usage.sh +86 -0
  66. data/examples/ler/README.md +111 -0
  67. data/examples/ler/simple_example.ler +0 -0
  68. data/examples/ler/simple_schema.exp +33 -0
  69. data/examples/ler_build.rb +75 -0
  70. data/examples/ler_cli.rb +79 -0
  71. data/examples/ler_demo_complete.rb +276 -0
  72. data/examples/ler_query.rb +91 -0
  73. data/examples/ler_query_examples.rb +305 -0
  74. data/examples/ler_stats.rb +81 -0
  75. data/examples/phase3_demo.rb +159 -0
  76. data/examples/query_demo_simple.rb +131 -0
  77. data/expressir.gemspec +2 -0
  78. data/lib/expressir/cli.rb +12 -4
  79. data/lib/expressir/commands/manifest.rb +427 -0
  80. data/lib/expressir/commands/package.rb +1274 -0
  81. data/lib/expressir/commands/validate.rb +70 -37
  82. data/lib/expressir/commands/validate_ascii.rb +607 -0
  83. data/lib/expressir/commands/validate_load.rb +88 -0
  84. data/lib/expressir/express/formatter.rb +5 -1
  85. data/lib/expressir/express/formatters/remark_item_formatter.rb +25 -0
  86. data/lib/expressir/express/parser.rb +33 -0
  87. data/lib/expressir/manifest/resolver.rb +213 -0
  88. data/lib/expressir/manifest/validator.rb +195 -0
  89. data/lib/expressir/model/declarations/entity.rb +6 -0
  90. data/lib/expressir/model/dependency_resolver.rb +270 -0
  91. data/lib/expressir/model/indexes/entity_index.rb +103 -0
  92. data/lib/expressir/model/indexes/reference_index.rb +148 -0
  93. data/lib/expressir/model/indexes/type_index.rb +149 -0
  94. data/lib/expressir/model/interface_validator.rb +384 -0
  95. data/lib/expressir/model/repository.rb +400 -5
  96. data/lib/expressir/model/repository_validator.rb +295 -0
  97. data/lib/expressir/model/search_engine.rb +525 -0
  98. data/lib/expressir/model.rb +4 -94
  99. data/lib/expressir/package/builder.rb +200 -0
  100. data/lib/expressir/package/metadata.rb +81 -0
  101. data/lib/expressir/package/reader.rb +165 -0
  102. data/lib/expressir/schema_manifest.rb +11 -1
  103. data/lib/expressir/version.rb +1 -1
  104. data/lib/expressir.rb +15 -2
  105. metadata +114 -4
  106. data/docs/benchmarking.adoc +0 -107
  107. data/docs/liquid_drops.adoc +0 -1547
@@ -0,0 +1,1038 @@
1
+ ---
2
+ title: Programmatic Usage
3
+ parent: Changes
4
+ grand_parent: Guides
5
+ nav_order: 4
6
+ ---
7
+
8
+ = Programmatic Usage
9
+
10
+ == Purpose
11
+
12
+ The Expressir Changes API provides Ruby classes for programmatically creating,
13
+ reading, updating, and managing EXPRESS Changes files. This guide covers the
14
+ complete Ruby API for working with changes.
15
+
16
+ == References
17
+
18
+ * link:index.html[Changes Overview] - Introduction to Changes files
19
+ * link:changes-format.html[Changes Format] - YAML format specification
20
+ * link:validating-changes.html[Validating Changes] - Validation guide
21
+ * link:importing-eengine.html[Importing from Express Engine] - Import guide
22
+
23
+ == Concepts
24
+
25
+ SchemaChange:: Top-level class representing a complete Changes file for a
26
+ schema
27
+
28
+ VersionChange:: Class representing changes for a specific version
29
+
30
+ ItemChange:: Class representing a single change to an EXPRESS construct
31
+
32
+ MappingChange:: Class representing a mapping-related change
33
+
34
+ round-trip serialization:: Loading from YAML and saving back to YAML with
35
+ consistent formatting
36
+
37
+ == Core classes
38
+
39
+ The Changes API consists of four main model classes:
40
+
41
+ [`Expressir::Changes::SchemaChange`](../../references/data-model/changes.html#schema-change)::
42
+ Top-level container for all changes to a schema
43
+
44
+ [`Expressir::Changes::VersionChange`](../../references/data-model/changes.html#version-change)::
45
+ Changes for a specific version
46
+
47
+ [`Expressir::Changes::ItemChange`](../../references/data-model/changes.html#item-change)::
48
+ Individual change to an EXPRESS construct
49
+
50
+ [`Expressir::Changes::MappingChange`](../../references/data-model/changes.html#mapping-change)::
51
+ Mapping-related change for ARM/MIM schemas
52
+
53
+ == Reading Changes files
54
+
55
+ === Loading from file
56
+
57
+ Load an existing Changes file:
58
+
59
+ [source,ruby]
60
+ ----
61
+ require "expressir/changes"
62
+
63
+ # Load from file
64
+ changes = Expressir::Changes::SchemaChange.from_file(
65
+ "schema.changes.yaml"
66
+ )
67
+
68
+ # Access top-level properties
69
+ puts "Schema: #{changes.schema}"
70
+ puts "Versions: #{changes.versions&.size || 0}"
71
+ ----
72
+
73
+ === Accessing versions
74
+
75
+ Iterate through versions:
76
+
77
+ [source,ruby]
78
+ ----
79
+ changes = Expressir::Changes::SchemaChange.from_file(
80
+ "schema.changes.yaml"
81
+ )
82
+
83
+ # Iterate through all versions
84
+ changes.versions&.each do |version|
85
+ puts "Version #{version.version}"
86
+ puts " Description: #{version.description}"
87
+ puts " Additions: #{version.additions&.size || 0}"
88
+ puts " Modifications: #{version.modifications&.size || 0}"
89
+ puts " Deletions: #{version.deletions&.size || 0}"
90
+ end
91
+ ----
92
+
93
+ === Finding specific versions
94
+
95
+ Find a version by number:
96
+
97
+ [source,ruby]
98
+ ----
99
+ changes = Expressir::Changes::SchemaChange.from_file(
100
+ "schema.changes.yaml"
101
+ )
102
+
103
+ # Find version 3
104
+ version_3 = changes.versions&.find { |v| v.version == 3 }
105
+
106
+ if version_3
107
+ puts "Found version 3"
108
+ puts "Additions: #{version_3.additions&.size || 0}"
109
+ else
110
+ puts "Version 3 not found"
111
+ end
112
+ ----
113
+
114
+ === Accessing change items
115
+
116
+ Access individual changes:
117
+
118
+ [source,ruby]
119
+ ----
120
+ changes = Expressir::Changes::SchemaChange.from_file(
121
+ "schema.changes.yaml"
122
+ )
123
+
124
+ version = changes.versions&.first
125
+
126
+ # Access additions
127
+ version.additions&.each do |item|
128
+ puts "Added #{item.type}: #{item.name}"
129
+ if item.description
130
+ puts " Description:"
131
+ item.description.each { |desc| puts " - #{desc}" }
132
+ end
133
+ if item.interfaced_items
134
+ puts " Interfaced items: #{item.interfaced_items}"
135
+ end
136
+ end
137
+
138
+ # Access modifications
139
+ version.modifications&.each do |item|
140
+ puts "Modified #{item.type}: #{item.name}"
141
+ end
142
+
143
+ # Access deletions
144
+ version.deletions&.each do |item|
145
+ puts "Deleted #{item.type}: #{item.name}"
146
+ end
147
+ ----
148
+
149
+ === Handling empty files
150
+
151
+ Handle empty or minimal Changes files:
152
+
153
+ [source,ruby]
154
+ ----
155
+ changes = Expressir::Changes::SchemaChange.from_file(
156
+ "empty.changes.yaml"
157
+ )
158
+
159
+ # Check for versions
160
+ if changes.versions.nil? || changes.versions.empty?
161
+ puts "No versions found"
162
+ else
163
+ puts "Found #{changes.versions.size} versions"
164
+ end
165
+ ----
166
+
167
+ == Creating Changes files
168
+
169
+ === Creating from scratch
170
+
171
+ Create a new SchemaChange:
172
+
173
+ [source,ruby]
174
+ ----
175
+ require "expressir/changes"
176
+
177
+ # Create empty schema change
178
+ changes = Expressir::Changes::SchemaChange.new(
179
+ schema: "my_schema"
180
+ )
181
+
182
+ puts "Created changes for: #{changes.schema}"
183
+ ----
184
+
185
+ === Adding versions
186
+
187
+ Add a version with changes:
188
+
189
+ [source,ruby]
190
+ ----
191
+ changes = Expressir::Changes::SchemaChange.new(
192
+ schema: "my_schema"
193
+ )
194
+
195
+ # Create change items
196
+ new_entity = Expressir::Changes::ItemChange.new(
197
+ type: "ENTITY",
198
+ name: "new_product",
199
+ description: ["Represents product information"]
200
+ )
201
+
202
+ modified_function = Expressir::Changes::ItemChange.new(
203
+ type: "FUNCTION",
204
+ name: "validate_data",
205
+ description: [
206
+ "Updated parameters",
207
+ "Improved validation logic"
208
+ ]
209
+ )
210
+
211
+ # Add version with changes
212
+ changes.add_or_update_version(
213
+ "2", # Version number
214
+ "Added product entity", # Description
215
+ {
216
+ additions: [new_entity],
217
+ modifications: [modified_function],
218
+ deletions: []
219
+ }
220
+ )
221
+
222
+ puts "Added version 2"
223
+ ----
224
+
225
+ === Creating interface changes
226
+
227
+ Create USE_FROM and REFERENCE_FROM changes:
228
+
229
+ [source,ruby]
230
+ ----
231
+ # Create interface imports
232
+ use_from = Expressir::Changes::ItemChange.new(
233
+ type: "USE_FROM",
234
+ name: "geometry_schema",
235
+ interfaced_items: "point"
236
+ )
237
+
238
+ reference_from = Expressir::Changes::ItemChange.new(
239
+ type: "REFERENCE_FROM",
240
+ name: "measure_schema",
241
+ interfaced_items: "length_measure"
242
+ )
243
+
244
+ # Add to version
245
+ changes.add_or_update_version(
246
+ "2",
247
+ "Added external references",
248
+ {
249
+ additions: [use_from, reference_from]
250
+ }
251
+ )
252
+ ----
253
+
254
+ === Creating mapping changes
255
+
256
+ Create mapping changes for ARM/MIM:
257
+
258
+ [source,ruby]
259
+ ----
260
+ # Create mapping changes
261
+ mapping1 = Expressir::Changes::MappingChange.new(
262
+ name: "Product_entity",
263
+ description: "ENTITY mapping updated for new MIM structure"
264
+ )
265
+
266
+ mapping2 = Expressir::Changes::MappingChange.new(
267
+ name: "Identifier_type",
268
+ description: "TYPE mapping simplified"
269
+ )
270
+
271
+ # Add version with mappings
272
+ changes.add_or_update_version(
273
+ "2",
274
+ "Updated ARM mappings",
275
+ {
276
+ mappings: [mapping1, mapping2]
277
+ }
278
+ )
279
+ ----
280
+
281
+ === Complete example
282
+
283
+ Create a complete Changes file from scratch:
284
+
285
+ [source,ruby]
286
+ ----
287
+ require "expressir/changes"
288
+
289
+ # Create schema change
290
+ changes = Expressir::Changes::SchemaChange.new(
291
+ schema: "action_schema"
292
+ )
293
+
294
+ # Create version 2 changes
295
+ entity_addition = Expressir::Changes::ItemChange.new(
296
+ type: "ENTITY",
297
+ name: "action_status",
298
+ description: ["Tracks status of actions"]
299
+ )
300
+
301
+ function_mod = Expressir::Changes::ItemChange.new(
302
+ type: "FUNCTION",
303
+ name: "validate_action",
304
+ description: [
305
+ "Added status parameter",
306
+ "Improved validation"
307
+ ]
308
+ )
309
+
310
+ constant_deletion = Expressir::Changes::ItemChange.new(
311
+ type: "CONSTANT",
312
+ name: "old_status",
313
+ description: ["Replaced by action_status entity"]
314
+ )
315
+
316
+ # Add version 2
317
+ changes.add_or_update_version(
318
+ "2",
319
+ "Status tracking improvements",
320
+ {
321
+ additions: [entity_addition],
322
+ modifications: [function_mod],
323
+ deletions: [constant_deletion]
324
+ }
325
+ )
326
+
327
+ # Add version 3
328
+ interface_addition = Expressir::Changes::ItemChange.new(
329
+ type: "USE_FROM",
330
+ name: "support_resource_schema",
331
+ interfaced_items: "bag_to_set"
332
+ )
333
+
334
+ changes.add_or_update_version(
335
+ "3",
336
+ "Added external references",
337
+ {
338
+ additions: [interface_addition]
339
+ }
340
+ )
341
+
342
+ puts "Created changes with #{changes.versions.size} versions"
343
+ ----
344
+
345
+ == Updating Changes files
346
+
347
+ === Loading and modifying
348
+
349
+ Load an existing file and add changes:
350
+
351
+ [source,ruby]
352
+ ----
353
+ # Load existing file
354
+ changes = Expressir::Changes::SchemaChange.from_file(
355
+ "schema.changes.yaml"
356
+ )
357
+
358
+ puts "Current versions: #{changes.versions.size}"
359
+
360
+ # Add new version
361
+ new_item = Expressir::Changes::ItemChange.new(
362
+ type: "TYPE",
363
+ name: "status_enum",
364
+ description: ["New status enumeration"]
365
+ )
366
+
367
+ changes.add_or_update_version(
368
+ "4",
369
+ "Added status enumeration",
370
+ {
371
+ additions: [new_item]
372
+ }
373
+ )
374
+
375
+ puts "After update: #{changes.versions.size} versions"
376
+ ----
377
+
378
+ === Replacing existing versions
379
+
380
+ Replace an existing version:
381
+
382
+ [source,ruby]
383
+ ----
384
+ changes = Expressir::Changes::SchemaChange.from_file(
385
+ "schema.changes.yaml"
386
+ )
387
+
388
+ # Create new changes for version 2
389
+ revised_item = Expressir::Changes::ItemChange.new(
390
+ type: "ENTITY",
391
+ name: "revised_entity",
392
+ description: ["Corrected entity definition"]
393
+ )
394
+
395
+ # This replaces existing version 2
396
+ changes.add_or_update_version(
397
+ "2",
398
+ "Revised version 2 changes",
399
+ {
400
+ additions: [revised_item]
401
+ }
402
+ )
403
+
404
+ puts "Version 2 replaced"
405
+ ----
406
+
407
+ === Modifying existing items
408
+
409
+ Modify the changes directly:
410
+
411
+ [source,ruby]
412
+ ----
413
+ changes = Expressir::Changes::SchemaChange.from_file(
414
+ "schema.changes.yaml"
415
+ )
416
+
417
+ # Find and modify a version
418
+ version_2 = changes.versions&.find { |v| v.version == 2 }
419
+
420
+ if version_2
421
+ # Add to existing additions
422
+ version_2.additions ||= []
423
+ version_2.additions << Expressir::Changes::ItemChange.new(
424
+ type: "FUNCTION",
425
+ name: "new_function"
426
+ )
427
+
428
+ # Update description
429
+ version_2.description = "Enhanced version 2 with new function"
430
+
431
+ puts "Modified version 2"
432
+ end
433
+ ----
434
+
435
+ === Removing versions
436
+
437
+ Remove a version from the file:
438
+
439
+ [source,ruby]
440
+ ----
441
+ changes = Expressir::Changes::SchemaChange.from_file(
442
+ "schema.changes.yaml"
443
+ )
444
+
445
+ # Remove version 3
446
+ changes.versions&.reject! { |v| v.version == 3 }
447
+
448
+ puts "Removed version 3"
449
+ ----
450
+
451
+ == Saving Changes files
452
+
453
+ === Saving to file
454
+
455
+ Save a SchemaChange to a file:
456
+
457
+ [source,ruby]
458
+ ----
459
+ changes = Expressir::Changes::SchemaChange.new(
460
+ schema: "my_schema"
461
+ )
462
+
463
+ # ... add versions ...
464
+
465
+ # Save to file
466
+ changes.to_file("my_schema.changes.yaml")
467
+
468
+ puts "Saved to my_schema.changes.yaml"
469
+ ----
470
+
471
+ The `to_file` method:
472
+
473
+ * Automatically adds YAML schema hint comment
474
+ * Formats with consistent indentation
475
+ * Creates parent directories if needed
476
+ * Overwrites existing file
477
+
478
+ === Converting to YAML string
479
+
480
+ Get YAML as a string without saving:
481
+
482
+ [source,ruby]
483
+ ----
484
+ changes = Expressir::Changes::SchemaChange.new(
485
+ schema: "my_schema"
486
+ )
487
+
488
+ # Convert to YAML string
489
+ yaml_content = changes.to_yaml
490
+
491
+ puts yaml_content
492
+ ----
493
+
494
+ === Saving to stdout
495
+
496
+ Output to stdout for piping or display:
497
+
498
+ [source,ruby]
499
+ ----
500
+ changes = Expressir::Changes::SchemaChange.from_file(
501
+ "schema.changes.yaml"
502
+ )
503
+
504
+ # Output to stdout
505
+ puts changes.to_yaml
506
+ ----
507
+
508
+ == Version management strategies
509
+
510
+ === Sequential versioning
511
+
512
+ Maintain sequential version numbers:
513
+
514
+ [source,ruby]
515
+ ----
516
+ changes = Expressir::Changes::SchemaChange.from_file(
517
+ "schema.changes.yaml"
518
+ )
519
+
520
+ # Find highest version number
521
+ max_version = changes.versions&.map(&:version)&.max&.to_i || 1
522
+
523
+ # Add next version
524
+ next_version = max_version + 1
525
+ changes.add_or_update_version(
526
+ next_version,
527
+ "New features",
528
+ {
529
+ additions: [...]
530
+ }
531
+ )
532
+
533
+ puts "Added version #{next_version}"
534
+ ----
535
+
536
+ === Semantic versioning
537
+
538
+ Use semantic version strings:
539
+
540
+ [source,ruby]
541
+ ----
542
+ changes = Expressir::Changes::SchemaChange.new(
543
+ schema: "my_schema"
544
+ )
545
+
546
+ # Add semantic versions
547
+ changes.add_or_update_version(
548
+ "1.0.0",
549
+ "Initial release",
550
+ { additions: [...] }
551
+ )
552
+
553
+ changes.add_or_update_version(
554
+ "1.1.0",
555
+ "Minor update",
556
+ { modifications: [...] }
557
+ )
558
+
559
+ changes.add_or_update_version(
560
+ "2.0.0",
561
+ "Major update",
562
+ { additions: [...], deletions: [...] }
563
+ )
564
+ ----
565
+
566
+ === Version with metadata
567
+
568
+ Include detailed metadata in descriptions:
569
+
570
+ [source,ruby]
571
+ ----
572
+ changes.add_or_update_version(
573
+ "2",
574
+ "Version 2.0 - Released 2024-01-15\n" \
575
+ "Added support for new features.\n" \
576
+ "Breaking changes: Removed deprecated constants.",
577
+ {
578
+ additions: [...],
579
+ deletions: [...]
580
+ }
581
+ )
582
+ ----
583
+
584
+ === Branch-based versioning
585
+
586
+ Track changes for different branches:
587
+
588
+ [source,ruby]
589
+ ----
590
+ # Development version
591
+ changes.add_or_update_version(
592
+ "2-dev",
593
+ "Development changes",
594
+ { additions: [...] }
595
+ )
596
+
597
+ # Release candidate
598
+ changes.add_or_update_version(
599
+ "2-rc1",
600
+ "Release candidate 1",
601
+ { modifications: [...] }
602
+ )
603
+
604
+ # Final release
605
+ changes.add_or_update_version(
606
+ "2",
607
+ "Final release",
608
+ { ... }
609
+ )
610
+ ----
611
+
612
+ == Integration patterns
613
+
614
+ === Automated change tracking
615
+
616
+ Track changes automatically when schemas are modified:
617
+
618
+ [source,ruby]
619
+ ----
620
+ require "expressir"
621
+ require "expressir/changes"
622
+
623
+ class SchemaChangeTracker
624
+ def initialize(schema_name, changes_file)
625
+ @schema_name = schema_name
626
+ @changes_file = changes_file
627
+ @changes = load_or_create_changes
628
+ end
629
+
630
+ def track_addition(type, name, description)
631
+ item = Expressir::Changes::ItemChange.new(
632
+ type: type,
633
+ name: name,
634
+ description: description ? [description] : nil
635
+ )
636
+
637
+ @pending_additions << item
638
+ end
639
+
640
+ def track_modification(type, name, description)
641
+ item = Expressir::Changes::ItemChange.new(
642
+ type: type,
643
+ name: name,
644
+ description: description ? [description] : nil
645
+ )
646
+
647
+ @pending_modifications << item
648
+ end
649
+
650
+ def save_version(version, description)
651
+ @changes.add_or_update_version(
652
+ version,
653
+ description,
654
+ {
655
+ additions: @pending_additions,
656
+ modifications: @pending_modifications,
657
+ deletions: @pending_deletions
658
+ }
659
+ )
660
+
661
+ @changes.to_file(@changes_file)
662
+ clear_pending_changes
663
+ end
664
+
665
+ private
666
+
667
+ def load_or_create_changes
668
+ if File.exist?(@changes_file)
669
+ Expressir::Changes::SchemaChange.from_file(@changes_file)
670
+ else
671
+ Expressir::Changes::SchemaChange.new(schema: @schema_name)
672
+ end
673
+ end
674
+
675
+ def clear_pending_changes
676
+ @pending_additions = []
677
+ @pending_modifications = []
678
+ @pending_deletions = []
679
+ end
680
+ end
681
+
682
+ # Usage
683
+ tracker = SchemaChangeTracker.new(
684
+ "action_schema",
685
+ "action_schema.changes.yaml"
686
+ )
687
+
688
+ tracker.track_addition("ENTITY", "new_entity", "New entity added")
689
+ tracker.track_modification("FUNCTION", "validate", "Updated logic")
690
+ tracker.save_version("2", "Version 2 changes")
691
+ ----
692
+
693
+ === Report generation
694
+
695
+ Generate human-readable reports from changes:
696
+
697
+ [source,ruby]
698
+ ----
699
+ require "expressir/changes"
700
+
701
+ class ChangeReportGenerator
702
+ def initialize(changes_file)
703
+ @changes = Expressir::Changes::SchemaChange.from_file(changes_file)
704
+ end
705
+
706
+ def generate_report
707
+ report = []
708
+ report << "# Change Report for #{@changes.schema}"
709
+ report << ""
710
+
711
+ @changes.versions&.each do |version|
712
+ report << generate_version_section(version)
713
+ end
714
+
715
+ report.join("\n")
716
+ end
717
+
718
+ private
719
+
720
+ def generate_version_section(version)
721
+ lines = []
722
+ lines << "## Version #{version.version}"
723
+ lines << ""
724
+
725
+ if version.description
726
+ lines << version.description
727
+ lines << ""
728
+ end
729
+
730
+ if version.additions&.any?
731
+ lines << "### Additions"
732
+ version.additions.each do |item|
733
+ lines << "- #{item.type}: #{item.name}"
734
+ item.description&.each { |d| lines << " - #{d}" }
735
+ end
736
+ lines << ""
737
+ end
738
+
739
+ if version.modifications&.any?
740
+ lines << "### Modifications"
741
+ version.modifications.each do |item|
742
+ lines << "- #{item.type}: #{item.name}"
743
+ item.description&.each { |d| lines << " - #{d}" }
744
+ end
745
+ lines << ""
746
+ end
747
+
748
+ if version.deletions&.any?
749
+ lines << "### Deletions"
750
+ version.deletions.each do |item|
751
+ lines << "- #{item.type}: #{item.name}"
752
+ end
753
+ lines << ""
754
+ end
755
+
756
+ lines.join("\n")
757
+ end
758
+ end
759
+
760
+ # Usage
761
+ generator = ChangeReportGenerator.new("schema.changes.yaml")
762
+ report = generator.generate_report
763
+ File.write("CHANGELOG.md", report)
764
+ ----
765
+
766
+ === Comparing versions
767
+
768
+ Compare changes between versions:
769
+
770
+ [source,ruby]
771
+ ----
772
+ require "expressir/changes"
773
+
774
+ def compare_versions(changes, version1, version2)
775
+ v1 = changes.versions&.find { |v| v.version == version1 }
776
+ v2 = changes.versions&.find { |v| v.version == version2 }
777
+
778
+ return nil unless v1 && v2
779
+
780
+ {
781
+ version1: version1,
782
+ version2: version2,
783
+ added_in_v2: (v2.additions&.size || 0) - (v1.additions&.size || 0),
784
+ modified_in_v2: (v2.modifications&.size || 0) -
785
+ (v1.modifications&.size || 0),
786
+ deleted_in_v2: (v2.deletions&.size || 0) - (v1.deletions&.size || 0)
787
+ }
788
+ end
789
+
790
+ # Usage
791
+ changes = Expressir::Changes::SchemaChange.from_file(
792
+ "schema.changes.yaml"
793
+ )
794
+ comparison = compare_versions(changes, 2, 3)
795
+ puts "Changes from v2 to v3: #{comparison}"
796
+ ----
797
+
798
+ === Batch processing
799
+
800
+ Process multiple Changes files:
801
+
802
+ [source,ruby]
803
+ ----
804
+ require "expressir/changes"
805
+
806
+ def process_all_changes(directory)
807
+ Dir.glob("#{directory}/**/*.changes.yaml").each do |file|
808
+ begin
809
+ changes = Expressir::Changes::SchemaChange.from_file(file)
810
+
811
+ puts "File: #{file}"
812
+ puts " Schema: #{changes.schema}"
813
+ puts " Versions: #{changes.versions&.size || 0}"
814
+
815
+ # Process each version
816
+ changes.versions&.each do |version|
817
+ total_changes = (version.additions&.size || 0) +
818
+ (version.modifications&.size || 0) +
819
+ (version.deletions&.size || 0)
820
+ puts " v#{version.version}: #{total_changes} changes"
821
+ end
822
+
823
+ rescue StandardError => e
824
+ puts "Error processing #{file}: #{e.message}"
825
+ end
826
+ end
827
+ end
828
+
829
+ # Usage
830
+ process_all_changes("schemas/")
831
+ ----
832
+
833
+ == Error handling
834
+
835
+ === File loading errors
836
+
837
+ Handle file loading errors:
838
+
839
+ [source,ruby]
840
+ ----
841
+ require "expressir/changes"
842
+
843
+ begin
844
+ changes = Expressir::Changes::SchemaChange.from_file(
845
+ "schema.changes.yaml"
846
+ )
847
+ rescue Errno::ENOENT
848
+ puts "File not found"
849
+ # Create new file
850
+ changes = Expressir::Changes::SchemaChange.new(
851
+ schema: "default_schema"
852
+ )
853
+ rescue Psych::SyntaxError => e
854
+ puts "Invalid YAML syntax: #{e.message}"
855
+ exit 1
856
+ rescue StandardError => e
857
+ puts "Error loading file: #{e.message}"
858
+ exit 1
859
+ end
860
+ ----
861
+
862
+ === Validation errors
863
+
864
+ Validate data before using:
865
+
866
+ [source,ruby]
867
+ ----
868
+ def validate_change_item(item)
869
+ errors = []
870
+
871
+ errors << "Missing type" if item.type.nil? || item.type.empty?
872
+ errors << "Missing name" if item.name.nil? || item.name.empty?
873
+
874
+ valid_types = %w[
875
+ ENTITY TYPE FUNCTION PROCEDURE RULE CONSTANT
876
+ SUBTYPE_CONSTRAINT USE_FROM REFERENCE_FROM
877
+ ]
878
+
879
+ unless valid_types.include?(item.type)
880
+ errors << "Invalid type: #{item.type}"
881
+ end
882
+
883
+ errors
884
+ end
885
+
886
+ # Usage
887
+ item = Expressir::Changes::ItemChange.new(type: "ENTITY", name: "test")
888
+ errors = validate_change_item(item)
889
+
890
+ if errors.any?
891
+ puts "Validation errors:"
892
+ errors.each { |err| puts " - #{err}" }
893
+ else
894
+ puts "Item is valid"
895
+ end
896
+ ----
897
+
898
+ === Safe file writing
899
+
900
+ Write files safely with error handling:
901
+
902
+ [source,ruby]
903
+ ----
904
+ def save_changes_safely(changes, path)
905
+ # Create backup if file exists
906
+ if File.exist?(path)
907
+ backup_path = "#{path}.backup"
908
+ FileUtils.cp(path, backup_path)
909
+ puts "Created backup: #{backup_path}"
910
+ end
911
+
912
+ begin
913
+ changes.to_file(path)
914
+ puts "Saved to: #{path}"
915
+
916
+ # Remove backup on success
917
+ File.delete("#{path}.backup") if File.exist?("#{path}.backup")
918
+
919
+ rescue StandardError => e
920
+ puts "Error saving file: #{e.message}"
921
+
922
+ # Restore backup if it exists
923
+ if File.exist?("#{path}.backup")
924
+ FileUtils.mv("#{path}.backup", path)
925
+ puts "Restored from backup"
926
+ end
927
+
928
+ raise
929
+ end
930
+ end
931
+
932
+ # Usage
933
+ changes = Expressir::Changes::SchemaChange.new(schema: "test")
934
+ save_changes_safely(changes, "test.changes.yaml")
935
+ ----
936
+
937
+ == Best practices
938
+
939
+ === Use descriptive variable names
940
+
941
+ [source,ruby]
942
+ ----
943
+ # Good
944
+ entity_addition = Expressir::Changes::ItemChange.new(...)
945
+ function_modification = Expressir::Changes::ItemChange.new(...)
946
+
947
+ # Avoid
948
+ item1 = Expressir::Changes::ItemChange.new(...)
949
+ x = Expressir::Changes::ItemChange.new(...)
950
+ ----
951
+
952
+ === Initialize collections
953
+
954
+ Always initialize arrays before use:
955
+
956
+ [source,ruby]
957
+ ----
958
+ changes = Expressir::Changes::SchemaChange.new(schema: "test")
959
+ changes.versions ||= [] # Initialize if nil
960
+ ----
961
+
962
+ === Check for nil
963
+
964
+ Check for nil before accessing collections:
965
+
966
+ [source,ruby]
967
+ ----
968
+ # Safe
969
+ if changes.versions&.any?
970
+ changes.versions.each { |v| ... }
971
+ end
972
+
973
+ # Unsafe (may raise error)
974
+ changes.versions.each { |v| ... }
975
+ ----
976
+
977
+ === Use consistent version format
978
+
979
+ Choose a version format and stick with it:
980
+
981
+ [source,ruby]
982
+ ----
983
+ # Integer versions
984
+ changes.add_or_update_version(2, ...)
985
+ changes.add_or_update_version(3, ...)
986
+
987
+ # Or semantic versions
988
+ changes.add_or_update_version("1.0.0", ...)
989
+ changes.add_or_update_version("1.1.0", ...)
990
+
991
+ # Don't mix formats
992
+ ----
993
+
994
+ === Save after modifications
995
+
996
+ Always save after making changes:
997
+
998
+ [source,ruby]
999
+ ----
1000
+ changes = Expressir::Changes::SchemaChange.from_file("file.yaml")
1001
+ changes.add_or_update_version(...)
1002
+ changes.to_file("file.yaml") # Don't forget to save!
1003
+ ----
1004
+
1005
+ == Next steps
1006
+
1007
+ After mastering programmatic usage:
1008
+
1009
+ * link:changes-format.html[Changes Format] - Review format specification
1010
+ * link:validating-changes.html[Validating Changes] - Validate your changes
1011
+ * link:importing-eengine.html[Importing from Express Engine] - Import
1012
+ automation
1013
+
1014
+ == Summary
1015
+
1016
+ The Changes API provides comprehensive programmatic control:
1017
+
1018
+ * Four main classes: SchemaChange, VersionChange, ItemChange, MappingChange
1019
+ * Load from files with `from_file` method
1020
+ * Create changes programmatically with `new` and `add_or_update_version`
1021
+ * Save with `to_file` method
1022
+ * Version management strategies for different workflows
1023
+ * Integration patterns for automation
1024
+ * Proper error handling for robust applications
1025
+ * Best practices for maintainable code
1026
+
1027
+ Key takeaways:
1028
+
1029
+ * Use `SchemaChange.from_file` to load existing files
1030
+ * Use `add_or_update_version` for smart version handling
1031
+ * Always save with `to_file` after modifications
1032
+ * Check for nil before accessing collections
1033
+ * Handle errors appropriately
1034
+ * Use descriptive variable names
1035
+ * Initialize collections before use
1036
+ * Choose consistent version format
1037
+ * Validate data before using
1038
+ * Create backups before overwriting files