expressir 2.1.28 → 2.1.29

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2818d10a5113fdafcb93fef22c7520b2747b0aa6767003239e99266fbe28aacb
4
- data.tar.gz: 04d9cc82b902107a07f93114ee0a59f551a0969e158e0fa50a125d49d92dd097
3
+ metadata.gz: fbc607242e5dfe5db0276c14d37cdc1d38fc07e4f0014134b20105e443a3b96c
4
+ data.tar.gz: 32ba2fe11da1f6439b7a97e30215df127aa6826ae6199f205fa4376c7624a4d2
5
5
  SHA512:
6
- metadata.gz: b435f41cfa13599b1547769c1052c2e9ac800f86c3282ad6ef0a5608ba77b0ee86bf31fceed7c21595019aa79e87c8327f001dea6d03ba14d1dea7a024adc5a8
7
- data.tar.gz: 6c93af83e066f0288d41ec7552b4d512a0aa29626dfb8101f7268e92741989704e4131fbf76a976088b05a22187a952459461764bc18cda1f624380a46c92faf
6
+ metadata.gz: 06e09081cbb49ef2d4656e8f9104c830edf8fc0427ffbcef0f6fa686884468e561370656d74b3c0f893b1786e0e9d4d4d8f335d297e9e3ddb8211d9bc26bbfd1
7
+ data.tar.gz: 15350b06e45e272b7b7dbaee29c5030b794e67a30544344bf534ec2b86c89998fcb1123c1352201d4d41f217f8e11c3054f5e0ef3f94fccb96b3daa42aacda82
data/.rubocop_todo.yml CHANGED
@@ -1,23 +1,61 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-10-13 04:55:00 UTC using RuboCop version 1.81.1.
3
+ # on 2025-10-15 09:12:23 UTC using RuboCop version 1.81.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 565
9
+ # Offense count: 3
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
12
+ # SupportedStyles: with_first_argument, with_fixed_indentation
13
+ Layout/ArgumentAlignment:
14
+ Exclude:
15
+ - 'lib/expressir/commands/changes_import_eengine.rb'
16
+
17
+ # Offense count: 4
18
+ # This cop supports safe autocorrection (--autocorrect).
19
+ # Configuration parameters: EnforcedStyleAlignWith.
20
+ # SupportedStylesAlignWith: either, start_of_block, start_of_line
21
+ Layout/BlockAlignment:
22
+ Exclude:
23
+ - 'lib/expressir/commands/changes_import_eengine.rb'
24
+ - 'spec/expressir/commands/changes_import_eengine_spec.rb'
25
+
26
+ # Offense count: 4
27
+ # This cop supports safe autocorrection (--autocorrect).
28
+ Layout/BlockEndNewline:
29
+ Exclude:
30
+ - 'lib/expressir/commands/changes_import_eengine.rb'
31
+ - 'spec/expressir/commands/changes_import_eengine_spec.rb'
32
+
33
+ # Offense count: 8
34
+ # This cop supports safe autocorrection (--autocorrect).
35
+ # Configuration parameters: Width, AllowedPatterns.
36
+ Layout/IndentationWidth:
37
+ Exclude:
38
+ - 'lib/expressir/commands/changes_import_eengine.rb'
39
+ - 'spec/expressir/commands/changes_import_eengine_spec.rb'
40
+
41
+ # Offense count: 569
10
42
  # This cop supports safe autocorrection (--autocorrect).
11
43
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
12
44
  # URISchemes: http, https
13
45
  Layout/LineLength:
14
46
  Enabled: false
15
47
 
16
- # Offense count: 2
48
+ # Offense count: 3
49
+ # This cop supports safe autocorrection (--autocorrect).
50
+ # Configuration parameters: AllowInHeredoc.
51
+ Layout/TrailingWhitespace:
52
+ Exclude:
53
+ - 'lib/expressir/commands/changes_import_eengine.rb'
54
+
55
+ # Offense count: 1
17
56
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
18
57
  Lint/DuplicateBranch:
19
58
  Exclude:
20
- - 'lib/expressir/commands/changes_import_eengine.rb'
21
59
  - 'lib/expressir/coverage.rb'
22
60
 
23
61
  # Offense count: 2
@@ -40,7 +78,7 @@ Metrics/AbcSize:
40
78
  Metrics/BlockLength:
41
79
  Max: 46
42
80
 
43
- # Offense count: 59
81
+ # Offense count: 61
44
82
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
45
83
  Metrics/CyclomaticComplexity:
46
84
  Exclude:
@@ -58,12 +96,12 @@ Metrics/CyclomaticComplexity:
58
96
  - 'lib/expressir/model/model_element.rb'
59
97
  - 'spec/support/model_element_helper.rb'
60
98
 
61
- # Offense count: 110
99
+ # Offense count: 113
62
100
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
63
101
  Metrics/MethodLength:
64
102
  Max: 106
65
103
 
66
- # Offense count: 47
104
+ # Offense count: 48
67
105
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
68
106
  Metrics/PerceivedComplexity:
69
107
  Exclude:
@@ -134,6 +172,18 @@ RSpec/RepeatedExample:
134
172
  Exclude:
135
173
  - 'spec/expressir/model/data_types/logical_spec.rb'
136
174
 
175
+ # Offense count: 6
176
+ # This cop supports safe autocorrection (--autocorrect).
177
+ # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
178
+ # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
179
+ # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
180
+ # FunctionalMethods: let, let!, subject, watch
181
+ # AllowedMethods: lambda, proc, it
182
+ Style/BlockDelimiters:
183
+ Exclude:
184
+ - 'lib/expressir/commands/changes_import_eengine.rb'
185
+ - 'spec/expressir/commands/changes_import_eengine_spec.rb'
186
+
137
187
  # Offense count: 1
138
188
  # This cop supports safe autocorrection (--autocorrect).
139
189
  # Configuration parameters: EnforcedStyle, AllowComments.
@@ -146,11 +196,3 @@ Style/EmptyElse:
146
196
  Style/MissingRespondToMissing:
147
197
  Exclude:
148
198
  - 'lib/expressir/express/visitor.rb'
149
-
150
- # Offense count: 1
151
- # This cop supports unsafe autocorrection (--autocorrect-all).
152
- # Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments.
153
- # AllowedMethods: define_method
154
- Style/SymbolProc:
155
- Exclude:
156
- - 'spec/expressir/commands/changes_import_eengine_spec.rb'
data/README.adoc CHANGED
@@ -724,17 +724,40 @@ This is useful for:
724
724
  * **Catching errors early**: Validates before committing changes
725
725
  * **Cleaning up files**: Removes inconsistencies in formatting
726
726
 
727
- ==== Importing from eengine XML
727
+ ==== Importing from Express Engine comparison report XML
728
728
 
729
- The `changes import-eengine` command converts eengine comparison XML files to
730
- EXPRESS Changes YAML format.
729
+ The `changes import-eengine` command converts Express Engine (eengine) comparison
730
+ XML files to EXPRESS Changes YAML format.
731
731
 
732
732
  The eengine compare XML format is created through
733
- `exp-engine-engine/kernel/compare.lisp` in the EXPRESS Engine code. Expressir is
733
+ `exp-engine-engine/kernel/compare.lisp` in the Express Engine code. Expressir is
734
734
  compatible with v5.2.7 of eengine output.
735
735
 
736
+ The import command:
737
+
738
+ . Parses the eengine XML comparison file
739
+ . Automatically detects the XML mode (Schema/ARM/MIM)
740
+ . Extracts additions, modifications, and deletions
741
+ . Converts modified objects to EXPRESS Changes item format
742
+ . Preserves interface change information (`interfaced.items`)
743
+ . Creates or updates an EXPRESS Changes YAML file
744
+ . Supports appending new versions to existing files
745
+
746
+ When the output file already exists:
747
+
748
+ * **Same version**: Replaces the existing edition with that version
749
+ * **New version**: Adds a new edition to the file
750
+
751
+ This allows you to build up a complete change history incrementally across
752
+ multiple schema versions.
753
+
754
+ Syntax:
755
+
736
756
  [source, sh]
737
757
  ----
758
+ # Basic syntax
759
+ expressir changes import-eengine INPUT_XML SCHEMA_NAME VERSION [options]
760
+
738
761
  # Import and output to stdout
739
762
  expressir changes import-eengine comparison.xml schema_name "2"
740
763
 
@@ -748,26 +771,62 @@ expressir changes import-eengine comparison.xml schema_name "2" -o output.yaml -
748
771
  expressir changes import-eengine comparison.xml schema_name "3" -o existing.yaml
749
772
  ----
750
773
 
751
- [options="header"]
752
- |===
753
- | Option | Description
754
- | `-o, --output PATH` | Output YAML file path (stdout if not specified)
755
- | `--verbose` | Show verbose output
756
- |===
774
+ Where:
757
775
 
758
- The import command:
776
+ `INPUT_XML`:: Path to the eengine comparison XML file
777
+ `SCHEMA_NAME`:: Name of the schema being tracked (e.g., `aic_csg`, `action_schema`)
778
+ `VERSION`:: Version number for this set of changes (e.g., `"2"`, `"3"`)
759
779
 
760
- . Parses the eengine XML comparison file
761
- . Extracts additions, modifications, and deletions
762
- . Creates or updates an EXPRESS Changes YAML file
763
- . Supports appending new versions to existing files
780
+ Options:
764
781
 
765
- When the output file already exists:
782
+ `-o, --output PATH`:: Output YAML file path (stdout if not specified)
783
+ `--verbose`:: Show verbose output including parsed items
766
784
 
767
- * **Same version**: Replaces the existing edition with that version
768
- * **New version**: Adds a new edition to the file
785
+ The command automatically detects and handles all three eengine XML comparison
786
+ formats:
787
+
788
+ Schema mode:: `<schema.changes>` root element with `<schema.additions>`,
789
+ `<schema.modifications>`, and `<schema.deletions>` sections. See API docs for
790
+ details.
791
+
792
+ Example workflow for importing changes from multiple versions:
793
+
794
+ .Importing multiple versions incrementally
795
+ [example]
796
+ [source, sh]
797
+ ----
798
+ # Import version 2 changes
799
+ expressir changes import-eengine v2_comparison.xml action_schema "2" \
800
+ -o action_schema.changes.yaml
769
801
 
770
- This allows you to build up a complete change history incrementally.
802
+ # Import version 3 changes (appends to existing file)
803
+ expressir changes import-eengine v3_comparison.xml action_schema "3" \
804
+ -o action_schema.changes.yaml
805
+
806
+ # Import version 4 changes (appends to existing file)
807
+ expressir changes import-eengine v4_comparison.xml action_schema "4" \
808
+ -o action_schema.changes.yaml
809
+
810
+ # Verify the result
811
+ cat action_schema.changes.yaml
812
+ ----
813
+
814
+ This creates a single YAML file tracking changes across all three versions:
815
+
816
+ [source,yaml]
817
+ ----
818
+ schema: action_schema
819
+ editions:
820
+ - version: '2'
821
+ description: Changes from eengine comparison
822
+ additions: [...]
823
+ - version: '3'
824
+ description: Changes from eengine comparison
825
+ additions: [...]
826
+ - version: '4'
827
+ description: Changes from eengine comparison
828
+ additions: [...]
829
+ ----
771
830
 
772
831
 
773
832
  == Usage: Ruby
@@ -1171,7 +1230,7 @@ expressir coverage schemas.yml --format json --exclude=TYPE:SELECT
1171
1230
 
1172
1231
  Expressir provides the `Changes` module for managing and tracking changes to
1173
1232
  EXPRESS schemas across versions. This module implements the EXPRESS Changes YAML
1174
- format defined by ELF (Express Language Foundation).
1233
+ format defined by ELF (Express Language Foundation) (ELF 5005).
1175
1234
 
1176
1235
  The Changes module enables:
1177
1236
 
@@ -1180,6 +1239,205 @@ The Changes module enables:
1180
1239
  * Smart edition handling (replace same version, add new version)
1181
1240
  * Support for all change types: additions, modifications, deletions
1182
1241
  * Support for mapping changes in ARM/MIM schemas
1242
+ * Programmatic import from Express Engine comparison XML files
1243
+
1244
+
1245
+ === Importing from Express Engine XML programmatically
1246
+
1247
+ ==== General
1248
+
1249
+ Expressir provides programmatic API access for parsing Express Engine comparison XML
1250
+ and converting it to EXPRESS Changes format.
1251
+
1252
+ Parse Eengine XML into a structured object model:
1253
+
1254
+ [source,ruby]
1255
+ ----
1256
+ require "expressir/eengine/compare_report"
1257
+
1258
+ # Parse from XML string
1259
+ xml_content = File.read("comparison.xml")
1260
+ report = Expressir::Eengine::CompareReport.from_xml(xml_content)
1261
+
1262
+ # Or parse from file
1263
+ report = Expressir::Eengine::CompareReport.from_file("comparison.xml")
1264
+ ----
1265
+
1266
+ Convert Eengine XML directly to EXPRESS Changes format:
1267
+
1268
+ [source,ruby]
1269
+ ----
1270
+ require "expressir/commands/changes_import_eengine"
1271
+
1272
+ # Parse XML and convert to SchemaChange in one step
1273
+ xml_content = File.read("comparison.xml")
1274
+ change_schema = Expressir::Commands::ChangesImportEengine.from_xml(
1275
+ xml_content,
1276
+ "aic_csg", # schema name
1277
+ "1.0" # version
1278
+ )
1279
+
1280
+ # Use the SchemaChange object
1281
+ change_schema.editions.first.additions.each do |item|
1282
+ puts "#{item.type}: #{item.name}"
1283
+ puts " interfaced_items: #{item.interfaced_items}" if item.interfaced_items
1284
+ end
1285
+
1286
+ # Save to file
1287
+ change_schema.to_file("output.changes.yaml")
1288
+ ----
1289
+
1290
+ ==== Compare modes
1291
+
1292
+ ===== General
1293
+
1294
+ Expressir automatically detects and parses all three Eengine XML modes.
1295
+
1296
+ [source,ruby]
1297
+ ----
1298
+ # All modes are handled automatically
1299
+ arm_report = Expressir::Eengine::CompareReport.from_file("arm_comparison.xml")
1300
+ mim_report = Expressir::Eengine::CompareReport.from_file("mim_comparison.xml")
1301
+ schema_report = Expressir::Eengine::CompareReport.from_file("schema_comparison.xml")
1302
+
1303
+ puts arm_report.mode # => "arm"
1304
+ puts mim_report.mode # => "mim"
1305
+ puts schema_report.mode # => "schema"
1306
+ ----
1307
+
1308
+ ===== Schema mode
1309
+
1310
+ `<schema.changes>` with `<schema.additions>`, `<schema.modifications>`, `<schema.deletions>`
1311
+
1312
+ [example]
1313
+ ====
1314
+ [source,xml]
1315
+ ----
1316
+ <schema.changes schema_name="aic_csg">
1317
+ <schema.additions>
1318
+ <modified.object type="ENTITY" name="new_entity" />
1319
+ </schema.additions>
1320
+ <schema.modifications>
1321
+ <modified.object type="TYPE" name="modified_type" />
1322
+ </schema.modifications>
1323
+ <schema.deletions>
1324
+ <modified.object type="FUNCTION" name="removed_function" />
1325
+ </schema.deletions>
1326
+ </schema.changes>
1327
+ ----
1328
+ ====
1329
+
1330
+ ===== ARM mode
1331
+
1332
+ `<arm.changes>` root element with `<arm.additions>`,
1333
+ `<arm.modifications>`, and `<arm.deletions>` sections
1334
+
1335
+ [example]
1336
+ ====
1337
+ [source,xml]
1338
+ ----
1339
+ <arm.changes schema_name="example_arm">
1340
+ <arm.additions>
1341
+ <modified.object type="ENTITY" name="new_arm_entity" />
1342
+ </arm.additions>
1343
+ </arm.changes>
1344
+ ----
1345
+ ====
1346
+
1347
+
1348
+ ===== MIM mode
1349
+
1350
+ `<mim.changes>` root element with `<mim.additions>`,
1351
+ `<mim.modifications>`, and `<mim.deletions>` sections
1352
+
1353
+ [example]
1354
+ ====
1355
+ [source,xml]
1356
+ ----
1357
+ <mim.changes schema_name="example_mim">
1358
+ <mim.additions>
1359
+ <modified.object type="ENTITY" name="new_mim_entity" />
1360
+ </mim.additions>
1361
+ </mim.changes>
1362
+ ----
1363
+ ====
1364
+
1365
+ ==== Interface changes
1366
+
1367
+ The eengine XML format tracks interface changes (USE_FROM, REFERENCE_FROM) with
1368
+ the `interfaced.items` attribute. This attribute lists the specific items being
1369
+ imported or referenced from another schema.
1370
+
1371
+ .Example XML with interface changes
1372
+ [example]
1373
+ ====
1374
+ [source,xml]
1375
+ ----
1376
+ <schema.changes schema_name="aic_csg">
1377
+ <schema.additions>
1378
+ <modified.object type="USE_FROM" name="geometric_model_schema"
1379
+ interfaced.items="convex_hexahedron" />
1380
+
1381
+ <modified.object type="USE_FROM" name="geometric_model_schema"
1382
+ interfaced.items="cyclide_segment_solid" />
1383
+
1384
+ <modified.object type="REFERENCE_FROM" name="measure_schema"
1385
+ interfaced.items="length_measure" />
1386
+ </schema.additions>
1387
+ </schema.changes>
1388
+ ----
1389
+
1390
+ This will be converted to EXPRESS Changes YAML as:
1391
+
1392
+ [source,yaml]
1393
+ ----
1394
+ schema: aic_csg
1395
+ editions:
1396
+ - version: '2'
1397
+ description: Changes from eengine comparison
1398
+ additions:
1399
+ - type: USE_FROM
1400
+ name: geometric_model_schema
1401
+ interfaced_items: convex_hexahedron
1402
+ - type: USE_FROM
1403
+ name: geometric_model_schema
1404
+ interfaced_items: cyclide_segment_solid
1405
+ - type: REFERENCE_FROM
1406
+ name: measure_schema
1407
+ interfaced_items: length_measure
1408
+ ----
1409
+ ====
1410
+
1411
+ Eengine XML files track interface changes (USE_FROM, REFERENCE_FROM) with the
1412
+ `interfaced.items` attribute:
1413
+
1414
+ [source,ruby]
1415
+ ----
1416
+ report = Expressir::Eengine::CompareReport.from_file("comparison.xml")
1417
+
1418
+ # Find interface changes
1419
+ report.additions&.modified_objects&.each do |obj|
1420
+ if obj.type == "USE_FROM" || obj.type == "REFERENCE_FROM"
1421
+ puts "#{obj.type} #{obj.name}"
1422
+ puts " Items: #{obj.interfaced_items}" if obj.interfaced_items
1423
+ end
1424
+ end
1425
+ ----
1426
+
1427
+
1428
+ ==== Supported change types
1429
+
1430
+ The import command recognizes all standard EXPRESS construct types:
1431
+
1432
+ * `ENTITY` - Entity definitions
1433
+ * `TYPE` - Type definitions
1434
+ * `FUNCTION` - Function definitions
1435
+ * `PROCEDURE` - Procedure definitions
1436
+ * `RULE` - Rule definitions
1437
+ * `CONSTANT` - Constant definitions
1438
+ * `USE_FROM` - Interface imports (preserves `interfaced.items`)
1439
+ * `REFERENCE_FROM` - Interface references (preserves `interfaced.items`)
1440
+
1183
1441
 
1184
1442
  === Reading change files
1185
1443
 
@@ -1296,7 +1554,7 @@ Change editions support categorizing changes into:
1296
1554
  `additions`:: New elements added to the schema
1297
1555
  `modifications`:: Existing elements that were modified
1298
1556
  `deletions`:: Elements removed from the schema
1299
- `mapping`:: Mapping-related changes (for ARM/MIM modules)
1557
+ `mappings`:: Mapping-related changes (for ARM/MIM modules)
1300
1558
  `changes`:: General changes (alternative to mapping)
1301
1559
 
1302
1560
  [source,ruby]
@@ -1307,7 +1565,7 @@ edition = Expressir::Changes::EditionChange.new(
1307
1565
  additions: [item1, item2],
1308
1566
  modifications: [item3],
1309
1567
  deletions: [item4],
1310
- mapping: [mapping_change]
1568
+ mappings: [mapping_change]
1311
1569
  )
1312
1570
  ----
1313
1571
 
@@ -13,7 +13,7 @@ module Expressir
13
13
  attribute :additions, ItemChange, collection: true
14
14
  attribute :modifications, ItemChange, collection: true
15
15
  attribute :deletions, ItemChange, collection: true
16
- attribute :mapping, MappingChange, collection: true
16
+ attribute :mappings, MappingChange, collection: true
17
17
  attribute :changes, MappingChange, collection: true
18
18
 
19
19
  yaml do
@@ -22,7 +22,7 @@ module Expressir
22
22
  map "additions", to: :additions
23
23
  map "modifications", to: :modifications
24
24
  map "deletions", to: :deletions
25
- map "mapping", to: :mapping
25
+ map "mappings", to: :mappings
26
26
  map "changes", to: :changes
27
27
  end
28
28
  end
@@ -8,7 +8,7 @@ module Expressir
8
8
  class ItemChange < Lutaml::Model::Serializable
9
9
  attribute :type, :string
10
10
  attribute :name, :string
11
- attribute :description, :string
11
+ attribute :description, :string, collection: true
12
12
  attribute :interfaced_items, :string
13
13
 
14
14
  yaml do
@@ -6,11 +6,11 @@ module Expressir
6
6
  module Changes
7
7
  # Represents a mapping change entry
8
8
  class MappingChange < Lutaml::Model::Serializable
9
- attribute :change, :string
9
+ attribute :name, :string
10
10
  attribute :description, :string
11
11
 
12
12
  yaml do
13
- map "change", to: :change
13
+ map "name", to: :name
14
14
  map "description", to: :description
15
15
  end
16
16
  end
@@ -1,41 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
- require "moxml"
4
+ require_relative "../eengine/compare_report"
5
5
 
6
6
  module Expressir
7
7
  module Commands
8
8
  # Command to import eengine comparison XML to EXPRESS Changes YAML
9
9
  class ChangesImportEengine < Base
10
- def self.call(input_file, output_file, schema_name, version, **options)
11
- new.call(input_file, output_file, schema_name, version, **options)
10
+ # Parse XML string and convert to SchemaChange
11
+ #
12
+ # @param xml_content [String] Eengine XML content
13
+ # @param schema_name [String] Schema name
14
+ # @param version [String] Version identifier
15
+ # @param options [Hash] Additional options
16
+ # @return [Expressir::Changes::SchemaChange]
17
+ def self.from_xml(xml_content, schema_name, version, **options)
18
+ require "expressir/changes"
19
+
20
+ # Parse into CompareReport using Lutaml::Model
21
+ compare_report = Expressir::Eengine::CompareReport.from_xml(xml_content)
22
+
23
+ # Convert to SchemaChange
24
+ convert_to_schema_change(compare_report, schema_name, version,
25
+ xml_content: xml_content, **options)
12
26
  end
13
27
 
14
- def call(input_file, output_file, schema_name, version, **options)
28
+ # File-based workflow (backward compatible)
29
+ def self.call(input_file, output_file, schema_name, version, **options)
15
30
  require "expressir/changes"
16
31
 
17
- # Parse the eengine XML using Moxml
18
32
  xml_content = File.read(input_file)
19
- xml_doc = Moxml.new.parse(xml_content)
20
-
21
- # Detect XML mode from root element
22
- xml_mode = detect_xml_mode(xml_doc)
23
33
 
24
- # Extract changes from XML
25
- changes = extract_changes(xml_doc, xml_mode)
26
- description = generate_description(xml_doc, xml_mode)
34
+ # Load existing schema if output file exists
35
+ existing_schema = if output_file && File.exist?(output_file) && File.size(output_file).positive?
36
+ Expressir::Changes::SchemaChange.from_file(output_file)
37
+ end
27
38
 
28
- # Load or create change schema
29
- change_schema = if output_file && File.exist?(output_file) && File.size(output_file).positive?
30
- Expressir::Changes::SchemaChange.from_file(output_file)
31
- else
32
- Expressir::Changes::SchemaChange.new(schema: schema_name)
33
- end
39
+ change_schema = from_xml(xml_content, schema_name, version,
40
+ existing_schema: existing_schema, **options)
34
41
 
35
- # Add or update edition
36
- change_schema.add_or_update_edition(version, description, changes)
37
-
38
- # Save to file
39
42
  if output_file
40
43
  change_schema.to_file(output_file)
41
44
  puts "Change YAML file written to: #{output_file}" if options[:verbose]
@@ -46,91 +49,126 @@ module Expressir
46
49
  change_schema
47
50
  end
48
51
 
49
- private
52
+ class << self
53
+ private
50
54
 
51
- # Detect XML mode from root element (arm, mim, or schema)
52
- def detect_xml_mode(xml_doc)
53
- root = xml_doc.root
54
- return nil unless root
55
+ def convert_to_schema_change(compare_report, schema_name, version,
56
+ **options)
57
+ require "expressir/changes"
55
58
 
56
- case root.name
57
- when "arm.changes"
58
- "arm"
59
- when "mim.changes"
60
- "mim"
61
- when "schema.changes"
62
- "schema"
63
- else
64
- # Default to schema mode if unrecognized
65
- "schema"
66
- end
67
- end
59
+ # Extract changes from CompareReport
60
+ changes = {
61
+ additions: extract_items(compare_report.additions,
62
+ options[:xml_content]),
63
+ modifications: extract_items(compare_report.modifications,
64
+ options[:xml_content]),
65
+ deletions: extract_items(compare_report.deletions,
66
+ options[:xml_content]),
67
+ }
68
68
 
69
- def extract_changes(xml_doc, xml_mode)
70
- {
71
- additions: extract_added_objects(xml_doc, xml_mode),
72
- modifications: extract_modified_objects(xml_doc, xml_mode),
73
- deletions: extract_deleted_objects(xml_doc, xml_mode),
74
- }
75
- end
69
+ # Use existing schema or create new one
70
+ change_schema = options[:existing_schema] ||
71
+ Expressir::Changes::SchemaChange.new(schema: schema_name)
76
72
 
77
- def extract_modified_objects(xml_doc, xml_mode)
78
- xpath = "//#{xml_mode}.modifications/modified.object"
79
- xml_doc.xpath(xpath).map do |node|
80
- extract_item_change(node)
73
+ # No edition-level description from eengine (only item-level)
74
+ change_schema.add_or_update_edition(version, nil, changes)
75
+
76
+ change_schema
81
77
  end
82
- end
83
78
 
84
- def extract_added_objects(xml_doc, xml_mode)
85
- xpath = "//#{xml_mode}.additions/modified.object"
86
- xml_doc.xpath(xpath).map do |node|
87
- extract_item_change(node)
79
+ def extract_items(changes_section, xml_content)
80
+ return [] unless changes_section&.modified_objects
81
+
82
+ # Extract descriptions from XML as arrays
83
+ descriptions = extract_descriptions_from_xml(xml_content)
84
+
85
+ changes_section.modified_objects.map do |obj|
86
+ Expressir::Changes::ItemChange.new(
87
+ type: obj.type,
88
+ name: obj.name,
89
+ interfaced_items: obj.interfaced_items,
90
+ description: descriptions[obj.name],
91
+ )
92
+ end
88
93
  end
89
- end
90
94
 
91
- def extract_deleted_objects(xml_doc, xml_mode)
92
- xpath = "//#{xml_mode}.deletions/modified.object"
93
- xml_doc.xpath(xpath).map do |node|
94
- extract_item_change(node)
95
+ def extract_descriptions_from_xml(xml_content)
96
+ return {} unless xml_content
97
+
98
+ require "nokogiri"
99
+ doc = Nokogiri::XML(xml_content)
100
+
101
+ doc.xpath("//modified.object").each_with_object({}) do |node, result|
102
+ name = node["name"]
103
+ desc_node = node.at_xpath("description")
104
+ next unless desc_node
105
+
106
+ html = desc_node.inner_html.strip
107
+ next if html.empty?
108
+
109
+ # Extract <li> elements or use text content
110
+ li_elements = Nokogiri::HTML.fragment(html).css("li")
111
+ result[name] = if li_elements.any?
112
+ li_elements.map do |li|
113
+ li.text.strip
114
+ end.reject(&:empty?)
115
+ else
116
+ [Nokogiri::HTML.fragment(html).text.strip]
117
+ end
118
+ end
95
119
  end
96
- end
97
120
 
98
- def extract_item_change(node)
99
- item_change = Expressir::Changes::ItemChange.new(
100
- type: node["type"],
101
- name: node["name"],
102
- )
121
+ def extract_description(compare_report)
122
+ parts = []
103
123
 
104
- # Extract interfaced.items attribute if present (for interface changes)
105
- if node["interfaced.items"]
106
- item_change.interfaced_items = node["interfaced.items"]
107
- end
124
+ [compare_report.modifications, compare_report.additions,
125
+ compare_report.deletions].each do |section|
126
+ next unless section&.modified_objects
108
127
 
109
- item_change
110
- end
128
+ section.modified_objects.each do |obj|
129
+ next unless obj.description
111
130
 
112
- def generate_description(xml_doc, xml_mode)
113
- parts = []
131
+ description_text = normalize_description(obj.description)
132
+ next if description_text.strip.empty?
114
133
 
115
- # Get descriptions from modifications
116
- xml_doc.xpath("//#{xml_mode}.modifications/modified.object/description").each do |desc|
117
- text = desc.text.strip
118
- parts << text unless text.empty?
119
- end
134
+ parts << convert_html_to_asciidoc(description_text.strip)
135
+ end
136
+ end
120
137
 
121
- # Get descriptions from additions
122
- xml_doc.xpath("//#{xml_mode}.additions/modified.object/description").each do |desc|
123
- text = desc.text.strip
124
- parts << text unless text.empty?
138
+ parts.empty? ? nil : parts.join("\n\n")
125
139
  end
126
140
 
127
- # Get descriptions from deletions
128
- xml_doc.xpath("//#{xml_mode}.deletions/modified.object/description").each do |desc|
129
- text = desc.text.strip
130
- parts << text unless text.empty?
141
+ def normalize_description(description)
142
+ # Handle both String and Array (when XML has nested elements)
143
+ case description
144
+ when String
145
+ description
146
+ when Array
147
+ # Join array elements, handling nested structures
148
+ description.map { |elem| normalize_description(elem) }.join("\n")
149
+ when Hash
150
+ # Handle hash elements (from XML parsing)
151
+ if description.key?("__text__")
152
+ description["__text__"]
153
+ else
154
+ description.values.map { |v| normalize_description(v) }.join("\n")
155
+ end
156
+ else
157
+ description.to_s
158
+ end
131
159
  end
132
160
 
133
- parts.join("\n\n")
161
+ def convert_html_to_asciidoc(text)
162
+ # Convert <ul><li>...</li></ul> to AsciiDoc list format
163
+ text = text.gsub(%r{<ul>\s*}i, "")
164
+ text = text.gsub(%r{\s*</ul>}i, "")
165
+ text = text.gsub(%r{<li>(.*?)</li>}im) do
166
+ "* #{Regexp.last_match(1).strip}"
167
+ end
168
+
169
+ # Clean up any extra whitespace
170
+ text.strip
171
+ end
134
172
  end
135
173
  end
136
174
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "changes_section"
5
+ require_relative "modified_object"
6
+
7
+ module Expressir
8
+ module Eengine
9
+ # Represents an Eengine ARM comparison XML report
10
+ class ArmCompareReport < Lutaml::Model::Serializable
11
+ attribute :modifications, ChangesSection
12
+ attribute :additions, ChangesSection
13
+ attribute :deletions, ChangesSection
14
+
15
+ xml do
16
+ root "arm.changes"
17
+ map_element "arm.modifications", to: :modifications
18
+ map_element "arm.additions", to: :additions
19
+ map_element "arm.deletions", to: :deletions
20
+ end
21
+
22
+ def mode
23
+ "arm"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "modified_object"
5
+
6
+ module Expressir
7
+ module Eengine
8
+ # Represents a section of changes (modifications, additions, or deletions)
9
+ # in an Eengine comparison report
10
+ class ChangesSection < Lutaml::Model::Serializable
11
+ attribute :modified_objects, ModifiedObject, collection: true
12
+
13
+ xml do
14
+ root "changes.section"
15
+ map_element "modified.object", to: :modified_objects
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ Lutaml::Model::Config.configure do |config|
6
+ require "lutaml/model/xml_adapter/nokogiri_adapter"
7
+ config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
8
+ end
9
+
10
+ require_relative "changes_section"
11
+ require_relative "modified_object"
12
+ require_relative "arm_compare_report"
13
+ require_relative "mim_compare_report"
14
+
15
+ module Expressir
16
+ module Eengine
17
+ # Represents an Eengine comparison XML report
18
+ # Supports three modes: ARM, MIM, and Schema
19
+ class CompareReport < Lutaml::Model::Serializable
20
+ attribute :modifications, ChangesSection
21
+ attribute :additions, ChangesSection
22
+ attribute :deletions, ChangesSection
23
+
24
+ xml do
25
+ root "schema.changes"
26
+ map_element "schema.modifications", to: :modifications
27
+ map_element "schema.additions", to: :additions
28
+ map_element "schema.deletions", to: :deletions
29
+ end
30
+
31
+ class << self
32
+ # Parse XML and return appropriate report class based on mode
33
+ #
34
+ # @param xml_content [String] XML content
35
+ # @return [CompareReport, ArmCompareReport, MimCompareReport]
36
+ def from_xml(xml_content)
37
+ mode = detect_mode(xml_content)
38
+
39
+ case mode
40
+ when "arm"
41
+ ArmCompareReport.from_xml(xml_content)
42
+ when "mim"
43
+ MimCompareReport.from_xml(xml_content)
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ # Load a CompareReport from an XML file
50
+ #
51
+ # @param path [String] Path to the XML file
52
+ # @return [CompareReport] The loaded comparison report
53
+ def from_file(path)
54
+ from_xml(File.read(path))
55
+ end
56
+
57
+ private
58
+
59
+ # Detect XML mode from content
60
+ #
61
+ # @param xml_content [String] XML content
62
+ # @return [String] "arm", "mim", or "schema"
63
+ def detect_mode(xml_content)
64
+ if xml_content.include?("<arm.changes")
65
+ "arm"
66
+ elsif xml_content.include?("<mim.changes")
67
+ "mim"
68
+ else
69
+ "schema"
70
+ end
71
+ end
72
+ end
73
+
74
+ # Detect XML mode from the report
75
+ # @return [String] "arm", "mim", or "schema"
76
+ def mode
77
+ "schema"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "changes_section"
5
+ require_relative "modified_object"
6
+
7
+ module Expressir
8
+ module Eengine
9
+ # Represents an Eengine MIM comparison XML report
10
+ class MimCompareReport < Lutaml::Model::Serializable
11
+ attribute :modifications, ChangesSection
12
+ attribute :additions, ChangesSection
13
+ attribute :deletions, ChangesSection
14
+
15
+ xml do
16
+ root "mim.changes"
17
+ map_element "mim.modifications", to: :modifications
18
+ map_element "mim.additions", to: :additions
19
+ map_element "mim.deletions", to: :deletions
20
+ end
21
+
22
+ def mode
23
+ "mim"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Expressir
6
+ module Eengine
7
+ # Represents a modified EXPRESS object in an Eengine comparison report
8
+ class ModifiedObject < Lutaml::Model::Serializable
9
+ attribute :type, :string
10
+ attribute :name, :string
11
+ attribute :interfaced_items, :string
12
+ attribute :description, :string
13
+
14
+ xml do
15
+ root "modified.object"
16
+ map_attribute "type", to: :type
17
+ map_attribute "name", to: :name
18
+ map_attribute "interfaced.items", to: :interfaced_items
19
+ map_element "description", to: :description
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module Expressir
2
- VERSION = "2.1.28".freeze
2
+ VERSION = "2.1.29".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expressir
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.28
4
+ version: 2.1.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-13 00:00:00.000000000 Z
11
+ date: 2025-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -202,6 +202,11 @@ files:
202
202
  - lib/expressir/commands/version.rb
203
203
  - lib/expressir/config.rb
204
204
  - lib/expressir/coverage.rb
205
+ - lib/expressir/eengine/arm_compare_report.rb
206
+ - lib/expressir/eengine/changes_section.rb
207
+ - lib/expressir/eengine/compare_report.rb
208
+ - lib/expressir/eengine/mim_compare_report.rb
209
+ - lib/expressir/eengine/modified_object.rb
205
210
  - lib/expressir/express/cache.rb
206
211
  - lib/expressir/express/error.rb
207
212
  - lib/expressir/express/formatter.rb