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,1274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+ require "yaml"
6
+ require "pathname"
7
+ require_relative "base"
8
+ require_relative "../model/search_engine"
9
+
10
+ module Expressir
11
+ module Commands
12
+ # Package management CLI commands
13
+ # Handles LER package creation, inspection, validation, and extraction
14
+ class Package < Thor
15
+ include Thor::Actions
16
+
17
+ # Allow options to be set for testing
18
+ attr_accessor :options
19
+
20
+ def initialize(options_or_args = [], *args, **kwargs)
21
+ # Check if first argument is options hash (for testing)
22
+ if options_or_args.is_a?(Hash) && args.empty? && kwargs.empty?
23
+ # Direct options passed for testing - don't initialize Thor
24
+ @options = options_or_args
25
+ else
26
+ # Normal Thor initialization
27
+ super
28
+ @options ||= {}
29
+ end
30
+ end
31
+
32
+ desc "build ROOT_SCHEMA OUTPUT",
33
+ "Build LER package from schema with dependency resolution"
34
+ long_desc <<~DESC
35
+ Build an LER package from a root schema file or manifest.
36
+
37
+ Two modes:
38
+ 1. Auto-resolution: Parses root schema and resolves dependencies
39
+ 2. Manifest-based: Uses pre-generated manifest file
40
+
41
+ Manifest Verification (manifest-based mode only):
42
+ By default, manifests are verified for referential integrity before building.
43
+ This ensures all USE FROM and REFERENCE FROM declarations can be resolved.
44
+
45
+ Use --skip-verify to bypass verification (may result in incomplete packages).
46
+ Warning: Skipping verification may produce packages with missing dependencies
47
+ that are not fully internally consistent.
48
+
49
+ Options match the Ruby API Repository#export_to_package method:
50
+ - express_mode: How to bundle EXPRESS files
51
+ - resolution_mode: Whether to pre-serialize schemas
52
+ - serialization_format: Format for serialized data
53
+ - validate: Run validation before packaging
54
+ - base_dirs: Base directories for schema resolution (auto-resolution only)
55
+ - manifest: Use pre-generated manifest file (skips dependency resolution)
56
+ - skip_verify: Skip manifest verification (manifest-based only)
57
+
58
+ Example (auto-resolution):
59
+ expressir package build schemas/activity/mim.exp activity.ler \\
60
+ --name "Activity Module" \\
61
+ --version "1.0" \\
62
+ --validate
63
+
64
+ Example (manifest-based with verification):
65
+ expressir package build --manifest activity_manifest.yaml activity.ler \\
66
+ --validate
67
+
68
+ Example (manifest-based, skip verification):
69
+ expressir package build --manifest activity_manifest.yaml activity.ler \\
70
+ --skip-verify
71
+ DESC
72
+ option :name, type: :string, desc: "Package name for metadata"
73
+ option :version, type: :string, desc: "Package version for metadata",
74
+ default: "1.0.0"
75
+ option :description, type: :string,
76
+ desc: "Package description for metadata"
77
+ option :express_mode, type: :string, default: "include_all",
78
+ enum: ["include_all", "allow_external"],
79
+ desc: "EXPRESS bundling mode"
80
+ option :resolution_mode, type: :string, default: "resolved",
81
+ enum: ["resolved", "bare"],
82
+ desc: "Schema resolution mode"
83
+ option :serialization_format, type: :string, default: "marshal",
84
+ enum: ["marshal", "json", "yaml"],
85
+ desc: "Serialization format for resolved mode"
86
+ option :validate, type: :boolean, default: true,
87
+ desc: "Validate repository before packaging"
88
+ option :base_dirs, type: :string,
89
+ desc: "Comma-separated list of base directories for schema resolution (auto-resolution only)"
90
+ option :manifest, type: :string,
91
+ desc: "Use pre-generated manifest file (skips dependency resolution)"
92
+ option :skip_verify, type: :boolean, default: false,
93
+ desc: "Skip manifest verification (may result in incomplete packages)"
94
+ option :verbose, type: :boolean, default: false,
95
+ desc: "Enable verbose output"
96
+ def build(root_schema = nil, output = nil)
97
+ require_relative "../model/dependency_resolver"
98
+ require_relative "../model/repository"
99
+ require_relative "../schema_manifest"
100
+
101
+ schema_files = if options[:manifest]
102
+ # Manifest-based mode
103
+ unless File.exist?(options[:manifest])
104
+ say "Error: Manifest file not found: #{options[:manifest]}",
105
+ :red
106
+ exit 1
107
+ end
108
+
109
+ # Override output if not provided
110
+ output ||= root_schema
111
+ unless output
112
+ say "Error: OUTPUT path is required", :red
113
+ say "Usage: expressir package build --manifest MANIFEST.yaml OUTPUT.ler",
114
+ :yellow
115
+ exit 1
116
+ end
117
+
118
+ # Build repository
119
+ say "Building LER package from manifest #{options[:manifest]}..." if options[:verbose]
120
+
121
+ # Load manifest for verification and data extraction
122
+ manifest = Expressir::SchemaManifest.from_file(options[:manifest])
123
+ manifest_data = YAML.load_file(options[:manifest])
124
+ manifest_dir = File.dirname(File.expand_path(options[:manifest]))
125
+
126
+ # Verify manifest unless --skip-verify is used
127
+ if options[:skip_verify]
128
+ say "⚠ Warning: Skipping manifest verification",
129
+ :yellow
130
+ say " This set of EXPRESS schemas may not be fully internally consistent.",
131
+ :yellow
132
+ say ""
133
+ else
134
+ say "Verifying manifest integrity..."
135
+ require_relative "../manifest/validator"
136
+
137
+ validator = Expressir::Manifest::Validator.new(
138
+ manifest, options.merge(verbose: true)
139
+ )
140
+
141
+ # Check file existence
142
+ file_errors = validator.validate_file_existence
143
+ unless file_errors.empty?
144
+ say "✗ Manifest validation failed", :red
145
+ say ""
146
+ file_errors.each do |e|
147
+ say " - #{e[:message]}", :red
148
+ end
149
+ exit 1
150
+ end
151
+
152
+ # Check referential integrity
153
+ reference_errors = validator.validate_referential_integrity
154
+ unless reference_errors.empty?
155
+ say "✗ Manifest has unresolved dependencies", :red
156
+ say ""
157
+ say "The following schema references cannot be resolved:",
158
+ :red
159
+ reference_errors.each do |e|
160
+ say " - #{e[:message]}", :red
161
+ end
162
+ say ""
163
+ say "This package may be incomplete or inconsistent.",
164
+ :red
165
+ say "To build anyway, use: --skip-verify", :yellow
166
+ exit 1
167
+ end
168
+
169
+ say "✓ Manifest verified", :green
170
+ end
171
+
172
+ # Validate and collect paths
173
+ errors = []
174
+ warnings = []
175
+ paths = []
176
+
177
+ manifest_data["schemas"].each do |schema_id, schema_data|
178
+ schema_path = schema_data["path"]
179
+
180
+ if schema_path.nil? || schema_path.empty?
181
+ warnings << "Schema '#{schema_id}' has no path specified"
182
+ next
183
+ end
184
+
185
+ # Expand relative paths relative to manifest directory
186
+ full_path = if Pathname.new(schema_path).absolute?
187
+ schema_path
188
+ else
189
+ File.expand_path(schema_path,
190
+ manifest_dir)
191
+ end
192
+
193
+ unless File.exist?(full_path)
194
+ errors << "Schema file not found: #{full_path} (#{schema_id})"
195
+ next
196
+ end
197
+
198
+ paths << full_path
199
+ end
200
+
201
+ unless errors.empty?
202
+ say "Error: Manifest validation failed", :red
203
+ errors.each { |e| say " - #{e}", :red }
204
+ exit 1
205
+ end
206
+
207
+ if options[:verbose] && warnings.any?
208
+ say "Warnings:"
209
+ warnings.each { |w| say " - #{w}", :yellow }
210
+ end
211
+
212
+ say " Using #{paths.size} schema(s) from manifest" if options[:verbose]
213
+ paths
214
+ else
215
+ # Auto-resolution mode
216
+ unless root_schema
217
+ say "Error: ROOT_SCHEMA is required when not using --manifest",
218
+ :red
219
+ say "Usage: expressir package build ROOT_SCHEMA OUTPUT.ler",
220
+ :yellow
221
+ exit 1
222
+ end
223
+
224
+ unless output
225
+ say "Error: OUTPUT path is required", :red
226
+ say "Usage: expressir package build ROOT_SCHEMA OUTPUT.ler",
227
+ :yellow
228
+ exit 1
229
+ end
230
+
231
+ say "Building LER package from #{root_schema}..." if options[:verbose]
232
+
233
+ # Resolve dependencies
234
+ say "Resolving dependencies..." if options[:verbose]
235
+ base_dirs = if options[:base_dirs]
236
+ options[:base_dirs].split(",").map(&:strip)
237
+ else
238
+ # Default to the directory containing the root schema
239
+ [File.dirname(File.expand_path(root_schema))]
240
+ end
241
+
242
+ if options[:verbose] && base_dirs.size == 1
243
+ say " Using base directory: #{base_dirs.first}"
244
+ say " Tip: Use --base-dirs to specify additional search paths for dependencies"
245
+ end
246
+
247
+ resolver = Expressir::Model::DependencyResolver.new(
248
+ base_dirs: base_dirs,
249
+ verbose: options[:verbose],
250
+ )
251
+ resolved = resolver.resolve_dependencies(root_schema)
252
+ say " Found #{resolved.size} schema(s)" if options[:verbose]
253
+ resolved
254
+ end
255
+
256
+ # Build repository
257
+ say "Building repository..." if options[:verbose]
258
+ repo = Expressir::Model::Repository.from_files(schema_files)
259
+
260
+ # Validate if requested
261
+ # When using --skip-verify, skip validation unless explicitly requested with --validate
262
+ should_validate = if options[:manifest] && options[:skip_verify]
263
+ # Check if --validate was explicitly passed
264
+ # Thor doesn't have a way to check if option was set by user vs default
265
+ # So we check if --no-validate was explicitly disabled
266
+ false
267
+ else
268
+ options[:validate]
269
+ end
270
+
271
+ if should_validate
272
+ say "Validating repository..." if options[:verbose]
273
+ validation = repo.validate
274
+ unless validation[:valid?]
275
+ say "✗ Repository validation failed", :red
276
+ say ""
277
+ errors = validation[:errors] || []
278
+ say "Validation errors (#{errors.size}):", :red
279
+ errors.each_with_index do |e, i|
280
+ error_msg = if e.is_a?(Hash)
281
+ # Format hash errors properly
282
+ msg = e[:message] || "Unknown error"
283
+ type = e[:type] ? "[#{e[:type]}] " : ""
284
+ "#{type}#{msg}"
285
+ else
286
+ # Fallback for string errors
287
+ e.to_s
288
+ end
289
+ say " #{i + 1}. #{error_msg}", :red
290
+ if e.is_a?(Hash) && e[:schema]
291
+ say " Schema: #{e[:schema]}",
292
+ :red
293
+ end
294
+ if e.is_a?(Hash) && e[:referenced_schema]
295
+ say " Referenced: #{e[:referenced_schema]}",
296
+ :red
297
+ end
298
+ if e.is_a?(Hash) && e[:interface_type]
299
+ say " Interface: #{e[:interface_type]}",
300
+ :red
301
+ end
302
+ end
303
+ exit 1
304
+ end
305
+ say " ✓ Validation passed" if options[:verbose]
306
+ end
307
+
308
+ # Build package
309
+ say "Creating package..." if options[:verbose]
310
+ repo.export_to_package(output, build_package_options)
311
+
312
+ say "✓ Package created: #{output}", :green
313
+ say " Schemas: #{repo.schemas.size}", :green if options[:verbose]
314
+ rescue StandardError => e
315
+ say "Error building package: #{e.message}", :red
316
+ say e.backtrace.join("\n") if options[:verbose]
317
+ exit 1
318
+ end
319
+
320
+ desc "info PACKAGE", "Display package metadata and statistics"
321
+ long_desc <<~DESC
322
+ Display comprehensive information about an LER package including metadata,
323
+ statistics, and configuration.
324
+
325
+ Output formats:
326
+ - text: Human-readable formatted output (default)
327
+ - json: Machine-readable JSON
328
+ - yaml: YAML format
329
+
330
+ Example:
331
+ expressir package info activity.ler
332
+ expressir package info activity.ler --format json
333
+ DESC
334
+ option :format, type: :string, default: "text",
335
+ enum: ["text", "json", "yaml"],
336
+ desc: "Output format"
337
+ def info(package_path)
338
+ require_relative "../model/repository"
339
+ require_relative "../package/reader"
340
+
341
+ repo = load_package(package_path)
342
+ metadata = load_package_metadata(package_path)
343
+
344
+ case options[:format]
345
+ when "json"
346
+ output_json_info(metadata, repo)
347
+ when "yaml"
348
+ output_yaml_info(metadata, repo)
349
+ else
350
+ output_text_info(metadata, repo)
351
+ end
352
+ rescue StandardError => e
353
+ say "Error reading package info: #{e.message}", :red
354
+ exit 1
355
+ end
356
+
357
+ desc "validate PACKAGE", "Validate package structure and integrity"
358
+ long_desc <<~DESC
359
+ Validate an LER package structure, metadata, and repository consistency.
360
+
361
+ Checks performed:
362
+ - Package file integrity
363
+ - Metadata validation
364
+ - Schema completeness
365
+ - Reference resolution
366
+ - Optional interface validation
367
+ - Optional strict mode for enhanced validation
368
+
369
+ Validation options:
370
+ - --strict: Enable strict validation (unused schemas become warnings)
371
+ - --check-interfaces: Perform detailed interface validation
372
+ - --check-completeness: Check for schema completeness
373
+ - --check-duplicates: Check for duplicate interface aliases
374
+ - --check-self-references: Check for self-referencing interfaces
375
+ - --detailed: Show detailed validation reports with fix suggestions
376
+
377
+ Example:
378
+ expressir package validate activity.ler
379
+ expressir package validate activity.ler --strict
380
+ expressir package validate activity.ler --check-interfaces --detailed
381
+ expressir package validate activity.ler --strict --check-completeness --check-interfaces
382
+ DESC
383
+ option :strict, type: :boolean, default: false,
384
+ desc: "Enable strict validation (unused schemas)"
385
+ option :check_interfaces, type: :boolean, default: true,
386
+ desc: "Perform detailed interface validation"
387
+ option :check_completeness, type: :boolean, default: false,
388
+ desc: "Check for schema completeness"
389
+ option :check_duplicates, type: :boolean, default: false,
390
+ desc: "Check for duplicate interface aliases"
391
+ option :check_self_references, type: :boolean, default: false,
392
+ desc: "Check for self-referencing interfaces"
393
+ option :detailed, type: :boolean, default: false,
394
+ desc: "Show detailed validation reports with fix suggestions"
395
+ option :format, type: :string, default: "text",
396
+ enum: ["text", "json", "yaml"],
397
+ desc: "Output format"
398
+ def validate(package_path)
399
+ require_relative "../model/repository"
400
+
401
+ repo = load_package(package_path)
402
+ validation = repo.validate(
403
+ strict: options[:strict],
404
+ )
405
+
406
+ case options[:format]
407
+ when "json"
408
+ output_json_validation(validation)
409
+ when "yaml"
410
+ output_yaml_validation(validation)
411
+ else
412
+ output_text_validation(validation)
413
+ end
414
+
415
+ exit 1 unless validation[:valid?]
416
+ rescue StandardError => e
417
+ say "Error validating package: #{e.message}", :red
418
+ exit 1
419
+ end
420
+
421
+ desc "extract PACKAGE", "Extract package contents to directory"
422
+ long_desc <<~DESC
423
+ Extract all contents of an LER package to a specified directory.
424
+
425
+ Extracts:
426
+ - Metadata file
427
+ - EXPRESS schema files
428
+ - Serialized repository (if present)
429
+ - Index files
430
+ - Manifest
431
+
432
+ Example:
433
+ expressir package extract activity.ler --output extracted/
434
+ DESC
435
+ option :output, aliases: "-o", type: :string, required: true,
436
+ desc: "Output directory for extraction"
437
+ def extract(package_path)
438
+ require "zip"
439
+ require "fileutils"
440
+
441
+ unless options[:output]
442
+ say "Error: output directory is required", :red
443
+ say "Usage: expressir package extract PACKAGE --output OUTPUT_DIR",
444
+ :yellow
445
+ exit 1
446
+ end
447
+
448
+ output_dir = options[:output]
449
+ FileUtils.mkdir_p(output_dir)
450
+
451
+ Zip::File.open(package_path) do |zip|
452
+ zip.each do |entry|
453
+ dest_path = File.join(output_dir, entry.name)
454
+ FileUtils.mkdir_p(File.dirname(dest_path))
455
+ entry.extract(dest_path) unless File.exist?(dest_path)
456
+ end
457
+ end
458
+
459
+ say "✓ Package extracted to: #{output_dir}", :green
460
+ say " Files extracted: #{Dir.glob(File.join(output_dir, '**', '*')).select do |f|
461
+ File.file?(f)
462
+ end.size}", :green
463
+ rescue StandardError => e
464
+ say "Error extracting package: #{e.message}", :red
465
+ exit 1
466
+ end
467
+
468
+ desc "list PACKAGE", "List all elements of a specific type"
469
+ long_desc <<~DESC
470
+ List all elements of a specific type in the package.
471
+
472
+ Element types:
473
+ - schema, entity, type, attribute, derived_attribute, inverse_attribute
474
+ - function, procedure, rule, constant, parameter, variable
475
+ - where_rule, unique_rule, enumeration_item, interface
476
+
477
+ Type categories (for --type type):
478
+ - select, enumeration, aggregate, defined
479
+
480
+ Example:
481
+ expressir package list activity.ler
482
+ expressir package list activity.ler --type entity
483
+ expressir package list activity.ler --type type --category select
484
+ expressir package list activity.ler --type entity --schema action_schema
485
+ DESC
486
+ option :type, type: :string, default: "entity",
487
+ desc: "Element type to list"
488
+ option :category, type: :string,
489
+ desc: "Type category (for --type type)"
490
+ option :schema, type: :string,
491
+ desc: "Filter by schema name"
492
+ option :format, type: :string, default: "text",
493
+ enum: ["text", "json", "yaml"],
494
+ desc: "Output format"
495
+ option :show_path, type: :boolean, default: false,
496
+ desc: "Show full EXPRESS paths"
497
+ option :show_details, type: :boolean, default: false,
498
+ desc: "Show element details"
499
+ option :count_only, type: :boolean, default: false,
500
+ desc: "Show count only"
501
+ def list(package_path)
502
+ repo = load_package(package_path)
503
+ search_engine = Expressir::Model::SearchEngine.new(repo)
504
+
505
+ results = search_engine.list(
506
+ type: options[:type],
507
+ schema: options[:schema],
508
+ category: options[:category],
509
+ )
510
+
511
+ if options[:count_only]
512
+ say results.size.to_s
513
+ return
514
+ end
515
+
516
+ case options[:format]
517
+ when "json"
518
+ say results.to_json
519
+ when "yaml"
520
+ say results.to_yaml
521
+ else
522
+ output_text_list(results, options[:type], options[:schema],
523
+ options[:category])
524
+ end
525
+ rescue StandardError => e
526
+ say "Error listing elements: #{e.message}", :red
527
+ exit 1
528
+ end
529
+
530
+ desc "search PACKAGE PATTERN", "Search for elements matching a pattern"
531
+ long_desc <<~DESC
532
+ Search for elements matching a pattern.
533
+
534
+ Pattern formats:
535
+ - name Simple name (searches all schemas)
536
+ - schema.element Qualified name
537
+ - schema.entity.attr Deep path
538
+ - *.element Wildcard schema
539
+ - schema.* Wildcard element
540
+ - *.*.name Multi-level wildcards
541
+ - action* Prefix matching
542
+
543
+ Example:
544
+ expressir package search activity.ler "action"
545
+ expressir package search activity.ler "action" --type entity
546
+ expressir package search activity.ler "*.*.id" --type attribute
547
+ expressir package search activity.ler "action*" --type entity
548
+ expressir package search activity.ler "action_schema.action.*" --type attribute
549
+ DESC
550
+ option :type, type: :string,
551
+ desc: "Filter by element type"
552
+ option :category, type: :string,
553
+ desc: "Type category (for --type type)"
554
+ option :schema, type: :string,
555
+ desc: "Limit to specific schema"
556
+ option :case_sensitive, type: :boolean, default: false,
557
+ desc: "Enable case-sensitive matching"
558
+ option :regex, type: :boolean, default: false,
559
+ desc: "Treat pattern as regex"
560
+ option :exact, type: :boolean, default: false,
561
+ desc: "Exact match only"
562
+ option :format, type: :string, default: "text",
563
+ enum: ["text", "json", "yaml"],
564
+ desc: "Output format"
565
+ option :show_path, type: :boolean, default: false,
566
+ desc: "Show full EXPRESS paths"
567
+ option :show_details, type: :boolean, default: false,
568
+ desc: "Show element details"
569
+ option :show_location, type: :boolean, default: false,
570
+ desc: "Show source location"
571
+ option :limit, type: :numeric,
572
+ desc: "Limit results"
573
+ option :count_only, type: :boolean, default: false,
574
+ desc: "Show count only"
575
+ def search(package_path, pattern)
576
+ repo = load_package(package_path)
577
+ search_engine = Expressir::Model::SearchEngine.new(repo)
578
+
579
+ results = search_engine.search(
580
+ pattern: pattern,
581
+ type: options[:type],
582
+ schema: options[:schema],
583
+ category: options[:category],
584
+ case_sensitive: options[:case_sensitive],
585
+ regex: options[:regex],
586
+ exact: options[:exact],
587
+ )
588
+
589
+ # Apply limit if specified
590
+ results = results.take(options[:limit]) if options[:limit]
591
+
592
+ if options[:count_only]
593
+ say results.size.to_s
594
+ return
595
+ end
596
+
597
+ case options[:format]
598
+ when "json"
599
+ say results.to_json
600
+ when "yaml"
601
+ say results.to_yaml
602
+ else
603
+ output_text_search_results(results, pattern)
604
+ end
605
+ rescue StandardError => e
606
+ say "Error searching: #{e.message}", :red
607
+ exit 1
608
+ end
609
+ desc "tree PACKAGE", "Display hierarchical tree view of package contents"
610
+ long_desc <<~DESC
611
+ Display a hierarchical tree view of all package contents showing schemas,
612
+ entities, types, functions, and their nested elements.
613
+
614
+ Tree structure uses:
615
+ - ├─ for intermediate items
616
+ - └─ for last items
617
+ - │ for continuation lines
618
+
619
+ Colors (can be disabled with --no-color):
620
+ - Schemas: bold blue
621
+ - Entities: green
622
+ - Types: yellow
623
+ - Attributes: cyan
624
+ - Functions: magenta
625
+ - Procedures: magenta
626
+ - Rules: red
627
+
628
+ Example:
629
+ expressir package tree activity.ler
630
+ expressir package tree activity.ler --depth 2
631
+ expressir package tree activity.ler --schema action_schema
632
+ expressir package tree activity.ler --type entity
633
+ expressir package tree activity.ler --counts
634
+ expressir package tree activity.ler --no-color
635
+ DESC
636
+ option :depth, type: :numeric,
637
+ desc: "Limit tree depth (1=schemas, 2=entities/types, 3=attributes)"
638
+ option :type, type: :string,
639
+ desc: "Filter to show only specific element type (entity, type, function, etc.)"
640
+ option :schema, type: :string,
641
+ desc: "Show tree for specific schema only"
642
+ option :no_color, type: :boolean, default: false,
643
+ desc: "Disable colors"
644
+ option :counts, type: :boolean, default: false,
645
+ desc: "Show element counts at each level"
646
+ def tree(package_path)
647
+ require "paint"
648
+
649
+ repo = load_package(package_path)
650
+
651
+ # Filter schemas if requested
652
+ schemas = if options[:schema]
653
+ (repo.schemas || []).select { |s| s.id == options[:schema] }
654
+ else
655
+ repo.schemas || []
656
+ end
657
+
658
+ if schemas.empty?
659
+ say "No schemas found", :yellow
660
+ return
661
+ end
662
+
663
+ # Display package name
664
+ package_name = File.basename(package_path)
665
+ say colorize(package_name, :bold)
666
+
667
+ # Display tree
668
+ schemas.each_with_index do |schema, idx|
669
+ is_last_schema = idx == schemas.size - 1
670
+ display_schema_tree(schema, is_last_schema, "", 1)
671
+ end
672
+ rescue StandardError => e
673
+ say "Error displaying tree: #{e.message}", :red
674
+ exit 1
675
+ end
676
+
677
+ private
678
+
679
+ # Build package options from CLI options
680
+ # @return [Hash] Package options
681
+ def build_package_options
682
+ {
683
+ name: options[:name] || "Unnamed Package",
684
+ version: options[:version],
685
+ description: options[:description] || "",
686
+ express_mode: options[:express_mode],
687
+ resolution_mode: options[:resolution_mode],
688
+ serialization_format: options[:serialization_format],
689
+ }
690
+ end
691
+
692
+ # Load package and return repository
693
+ # @param package_path [String] Path to .ler file
694
+ # @return [Model::Repository] Loaded repository
695
+ def load_package(package_path)
696
+ unless File.exist?(package_path)
697
+ say "Package file not found: #{package_path}", :red
698
+ exit 1
699
+ end
700
+
701
+ Expressir::Model::Repository.from_package(package_path)
702
+ end
703
+
704
+ # Load package metadata without full repository
705
+ # @param package_path [String] Path to .ler file
706
+ # @return [Package::Metadata] Package metadata
707
+ def load_package_metadata(package_path)
708
+ require "zip"
709
+ require_relative "../package/metadata"
710
+
711
+ Zip::File.open(package_path) do |zip|
712
+ entry = zip.find_entry("metadata.yaml")
713
+ raise "Metadata not found in package" unless entry
714
+
715
+ Expressir::Package::Metadata.from_yaml(entry.get_input_stream.read)
716
+ end
717
+ end
718
+
719
+ # Output package info in text format
720
+ # @param metadata [Package::Metadata] Package metadata
721
+ # @param repo [Model::Repository] Repository instance
722
+ # @return [void]
723
+ def output_text_info(metadata, repo)
724
+ stats = repo.statistics
725
+
726
+ say "Package Information", :bold
727
+ say "=" * 50
728
+ say "Name: #{metadata.name}" if metadata.name && !metadata.name.empty?
729
+ say "Version: #{metadata.version}" if metadata.version && !metadata.version.empty?
730
+ say "Description: #{metadata.description}" if metadata.description && !metadata.description.to_s.empty?
731
+ say "Created: #{metadata.created_at}" if metadata.created_at && !metadata.created_at.to_s.empty?
732
+ say "Created by: #{metadata.created_by}" if metadata.created_by && !metadata.created_by.to_s.empty? && metadata.created_by != "expressir"
733
+ say ""
734
+ say "Configuration", :bold
735
+ say "-" * 50
736
+ say "Express mode: #{metadata.express_mode}" if metadata.express_mode && !metadata.express_mode.to_s.empty?
737
+ say "Resolution mode: #{metadata.resolution_mode}" if metadata.resolution_mode && !metadata.resolution_mode.to_s.empty?
738
+ say "Serialization format: #{metadata.serialization_format}" if metadata.serialization_format && !metadata.serialization_format.to_s.empty?
739
+ say ""
740
+ say "Statistics", :bold
741
+ say "-" * 50
742
+ say "Total schemas: #{stats[:total_schemas]}"
743
+ say "Total entities: #{stats[:total_entities]}"
744
+ say "Total types: #{stats[:total_types]}"
745
+ say "Total functions: #{stats[:total_functions]}"
746
+ say "Total rules: #{stats[:total_rules]}"
747
+ say "Total procedures: #{stats[:total_procedures]}"
748
+ end
749
+
750
+ # Output package info in JSON format
751
+ # @param metadata [Package::Metadata] Package metadata
752
+ # @param repo [Model::Repository] Repository instance
753
+ # @return [void]
754
+ def output_json_info(metadata, repo)
755
+ stats = repo.statistics
756
+ # Convert symbol keys to strings recursively, handle nil stats
757
+ stats_hash = stats.is_a?(Hash) ? stringify_keys(stats) : {}
758
+
759
+ info = {
760
+ "metadata" => stringify_keys(metadata.to_h),
761
+ "statistics" => stats_hash,
762
+ }
763
+ say info.to_json
764
+ end
765
+
766
+ # Output package info in YAML format
767
+ # @param metadata [Package::Metadata] Package metadata
768
+ # @param repo [Model::Repository] Repository instance
769
+ # @return [void]
770
+ def output_yaml_info(metadata, repo)
771
+ stats = repo.statistics
772
+ # Convert symbol keys to strings recursively, handle nil stats
773
+ stats_hash = stats.is_a?(Hash) ? stringify_keys(stats) : {}
774
+
775
+ info = {
776
+ "metadata" => stringify_keys(metadata.to_h),
777
+ "statistics" => stats_hash,
778
+ }
779
+ say info.to_yaml
780
+ end
781
+
782
+ # Recursively stringify hash keys
783
+ # @param obj [Object] Object to stringify
784
+ # @return [Object] Object with stringified keys
785
+ def stringify_keys(obj)
786
+ case obj
787
+ when Hash
788
+ obj.transform_keys(&:to_s).transform_values { |v| stringify_keys(v) }
789
+ when Array
790
+ obj.map { |v| stringify_keys(v) }
791
+ else
792
+ obj
793
+ end
794
+ end
795
+
796
+ # Display schema in tree format
797
+ # @param schema [Declarations::Schema] Schema to display
798
+ # @param is_last [Boolean] Whether this is the last item
799
+ # @param prefix [String] Current indentation prefix
800
+ # @param current_depth [Integer] Current tree depth
801
+ # @return [void]
802
+ def display_schema_tree(schema, is_last, prefix, current_depth)
803
+ return if options[:depth] && current_depth > options[:depth]
804
+ return if options[:type] && options[:type] != "schema"
805
+
806
+ # Build schema line
807
+ connector = is_last ? "└─ " : "├─ "
808
+ counts_text = build_counts_text(schema) if options[:counts]
809
+ schema_text = "#{schema.id} (schema)#{counts_text}"
810
+
811
+ say "#{prefix}#{connector}#{colorize(schema_text, :blue, :bold)}"
812
+
813
+ # Don't show children if depth limit reached
814
+ return if options[:depth] && current_depth >= options[:depth]
815
+
816
+ # Build new prefix for children
817
+ child_prefix = prefix + (is_last ? " " : "│ ")
818
+
819
+ # Collect all displayable children
820
+ children = collect_schema_children(schema)
821
+
822
+ # Display children
823
+ children.each_with_index do |child, idx|
824
+ is_last_child = idx == children.size - 1
825
+ display_element_tree(child[:element], child[:type], is_last_child,
826
+ child_prefix, current_depth + 1)
827
+ end
828
+ end
829
+
830
+ # Collect schema children based on filters
831
+ # @param schema [Declarations::Schema] Schema to collect from
832
+ # @return [Array<Hash>] Array of child elements with type
833
+ def collect_schema_children(schema)
834
+ children = []
835
+
836
+ # Add entities
837
+ if should_include_type?("entity") && schema.entities
838
+ schema.entities.each do |e|
839
+ children << { element: e, type: "entity" }
840
+ end
841
+ end
842
+
843
+ # Add types
844
+ if should_include_type?("type") && schema.types
845
+ schema.types.each { |t| children << { element: t, type: "type" } }
846
+ end
847
+
848
+ # Add functions
849
+ if should_include_type?("function") && schema.functions
850
+ schema.functions.each do |f|
851
+ children << { element: f, type: "function" }
852
+ end
853
+ end
854
+
855
+ # Add procedures
856
+ if should_include_type?("procedure") && schema.procedures
857
+ schema.procedures.each do |p|
858
+ children << { element: p, type: "procedure" }
859
+ end
860
+ end
861
+
862
+ # Add rules
863
+ if should_include_type?("rule") && schema.rules
864
+ schema.rules.each { |r| children << { element: r, type: "rule" } }
865
+ end
866
+
867
+ children
868
+ end
869
+
870
+ # Display element in tree format
871
+ # @param element [ModelElement] Element to display
872
+ # @param type [String] Element type
873
+ # @param is_last [Boolean] Whether this is the last item
874
+ # @param prefix [String] Current indentation prefix
875
+ # @param current_depth [Integer] Current tree depth
876
+ # @return [void]
877
+ def display_element_tree(element, type, is_last, prefix, current_depth)
878
+ return if options[:depth] && current_depth > options[:depth]
879
+
880
+ connector = is_last ? "└─ " : "├─ "
881
+ element_text = format_element_text(element, type)
882
+ color = element_color(type)
883
+
884
+ say "#{prefix}#{connector}#{colorize(element_text, color)}"
885
+
886
+ # Don't show children if depth limit reached
887
+ return if options[:depth] && current_depth >= options[:depth]
888
+
889
+ # Build new prefix for children
890
+ child_prefix = prefix + (is_last ? " " : "│ ")
891
+
892
+ # Display children based on element type
893
+ case type
894
+ when "entity"
895
+ display_entity_children(element, child_prefix, current_depth + 1)
896
+ when "type"
897
+ display_type_children(element, child_prefix, current_depth + 1)
898
+ when "function", "procedure"
899
+ display_callable_children(element, child_prefix, current_depth + 1)
900
+ end
901
+ end
902
+
903
+ # Display entity children (attributes)
904
+ # @param entity [Declarations::Entity] Entity to display
905
+ # @param prefix [String] Current indentation prefix
906
+ # @param current_depth [Integer] Current tree depth
907
+ # @return [void]
908
+ def display_entity_children(entity, prefix, current_depth)
909
+ return if options[:depth] && current_depth > options[:depth]
910
+
911
+ children = []
912
+
913
+ # Add explicit attributes
914
+ if should_include_type?("attribute") && entity.attributes
915
+ entity.attributes.each do |a|
916
+ children << { element: a, type: "attribute" }
917
+ end
918
+ end
919
+
920
+ # Add derived attributes
921
+ if should_include_type?("derived_attribute") && entity.derived_attributes
922
+ entity.derived_attributes.each do |a|
923
+ children << { element: a, type: "derived_attribute" }
924
+ end
925
+ end
926
+
927
+ # Add inverse attributes
928
+ if should_include_type?("inverse_attribute") && entity.inverse_attributes
929
+ entity.inverse_attributes.each do |a|
930
+ children << { element: a, type: "inverse_attribute" }
931
+ end
932
+ end
933
+
934
+ # Show summary if too many and not showing all
935
+ if children.size > 5 && !should_show_all_attributes?
936
+ # Show first few
937
+ children.take(3).each_with_index do |child, _idx|
938
+ display_element_tree(child[:element], child[:type], false, prefix,
939
+ current_depth)
940
+ end
941
+
942
+ # Show summary
943
+ remaining = children.size - 3
944
+ summary = "... (#{remaining} more #{pluralize('attribute',
945
+ remaining)})"
946
+ say "#{prefix}└─ #{colorize(summary, :gray)}"
947
+ else
948
+ # Show all
949
+ children.each_with_index do |child, idx|
950
+ is_last = idx == children.size - 1
951
+ display_element_tree(child[:element], child[:type], is_last,
952
+ prefix, current_depth)
953
+ end
954
+ end
955
+ end
956
+
957
+ # Display type children
958
+ # @param type_elem [Declarations::Type] Type to display
959
+ # @param prefix [String] Current indentation prefix
960
+ # @param current_depth [Integer] Current tree depth
961
+ # @return [void]
962
+ def display_type_children(type_elem, prefix, current_depth)
963
+ return if options[:depth] && current_depth > options[:depth]
964
+
965
+ # For SELECT types, show items
966
+ if type_elem.underlying_type.is_a?(Expressir::Model::DataTypes::Select) && type_elem.underlying_type.items
967
+ items = type_elem.underlying_type.items
968
+ if items.size > 5 && !should_show_all_attributes?
969
+ # Show first few items
970
+ items.take(3).each_with_index do |item, _idx|
971
+ connector = "├─ "
972
+ say "#{prefix}#{connector}#{colorize(item.ref || item.id, :cyan)}"
973
+ end
974
+
975
+ # Items summary
976
+ remaining = items.size - 3
977
+ items_summary = "... [#{remaining} more items]"
978
+ say "#{prefix}└─ #{colorize(items_summary, :gray)}"
979
+
980
+ else
981
+ items.each_with_index do |item, idx|
982
+ is_last = idx == items.size - 1
983
+ connector = is_last ? "└─ " : "├─ "
984
+ say "#{prefix}#{connector}#{colorize(item.ref || item.id, :cyan)}"
985
+ end
986
+ end
987
+ end
988
+ end
989
+
990
+ # Display function/procedure children (parameters)
991
+ # @param callable [Declarations::Function, Declarations::Procedure] Callable to display
992
+ # @param prefix [String] Current indentation prefix
993
+ # @param current_depth [Integer] Current tree depth
994
+ # @return [void]
995
+ def display_callable_children(callable, prefix, current_depth)
996
+ return if options[:depth] && current_depth > options[:depth]
997
+ return unless should_include_type?("parameter")
998
+ return unless callable.parameters
999
+
1000
+ callable.parameters.each_with_index do |param, idx|
1001
+ is_last = idx == callable.parameters.size - 1
1002
+ display_element_tree(param, "parameter", is_last, prefix,
1003
+ current_depth)
1004
+ end
1005
+ end
1006
+
1007
+ # Format element text with type information
1008
+ # @param element [ModelElement] Element to format
1009
+ # @param type [String] Element type
1010
+ # @return [String] Formatted text
1011
+ def format_element_text(element, type)
1012
+ case type
1013
+ when "entity", "type", "function", "procedure", "rule"
1014
+ "#{element.id} (#{type})"
1015
+ when "attribute", "derived_attribute", "inverse_attribute", "parameter"
1016
+ type_info = extract_type_info(element)
1017
+ "#{element.id} (#{type.sub('_attribute', '')}): #{type_info}"
1018
+ when "parameter"
1019
+ type_info = extract_type_info(element)
1020
+ "#{element.id} (parameter): #{type_info}"
1021
+ else
1022
+ "#{element.id} (#{type})"
1023
+ end
1024
+ end
1025
+
1026
+ # Extract type information from attribute/parameter
1027
+ # @param element [Attribute, Parameter] Element with type
1028
+ # @return [String] Type description
1029
+ def extract_type_info(element)
1030
+ return "ANY" unless element.type
1031
+
1032
+ type = element.type
1033
+ case type
1034
+ when Expressir::Model::DataTypes::Set
1035
+ "SET"
1036
+ when Expressir::Model::DataTypes::List
1037
+ "LIST"
1038
+ when Expressir::Model::DataTypes::Array
1039
+ "ARRAY"
1040
+ when Expressir::Model::References::SimpleReference
1041
+ type.ref || type.id
1042
+ when Expressir::Model::DataTypes::String
1043
+ "STRING"
1044
+ when Expressir::Model::DataTypes::Integer
1045
+ "INTEGER"
1046
+ when Expressir::Model::DataTypes::Real
1047
+ "REAL"
1048
+ when Expressir::Model::DataTypes::Boolean
1049
+ "BOOLEAN"
1050
+ when Expressir::Model::DataTypes::Logical
1051
+ "LOGICAL"
1052
+ when Expressir::Model::DataTypes::Number
1053
+ "NUMBER"
1054
+ else
1055
+ type.class.name.split("::").last
1056
+ end
1057
+ end
1058
+
1059
+ # Build counts text for schema
1060
+ # @param schema [Declarations::Schema] Schema
1061
+ # @return [String] Counts text
1062
+ def build_counts_text(schema)
1063
+ counts = []
1064
+ entities_count = schema.entities&.size || 0
1065
+ types_count = schema.types&.size || 0
1066
+ functions_count = schema.functions&.size || 0
1067
+
1068
+ if entities_count.positive?
1069
+ counts << "#{entities_count} #{pluralize('entity',
1070
+ entities_count)}"
1071
+ end
1072
+ if types_count.positive?
1073
+ counts << "#{types_count} #{pluralize('type',
1074
+ types_count)}"
1075
+ end
1076
+ if functions_count.positive?
1077
+ counts << "#{functions_count} #{pluralize('function',
1078
+ functions_count)}"
1079
+ end
1080
+
1081
+ counts.any? ? " [#{counts.join(', ')}]" : ""
1082
+ end
1083
+
1084
+ # Colorize text based on options
1085
+ # @param text [String] Text to colorize
1086
+ # @param color [Symbol] Color name
1087
+ # @param style [Symbol, nil] Optional style (bold, etc.)
1088
+ # @return [String] Colorized or plain text
1089
+ def colorize(text, color, style = nil)
1090
+ return "" if text.nil?
1091
+ return text if options[:no_color]
1092
+
1093
+ if style
1094
+ Paint[text, color, style]
1095
+ else
1096
+ Paint[text, color]
1097
+ end
1098
+ end
1099
+
1100
+ # Get color for element type
1101
+ # @param type [String] Element type
1102
+ # @return [Symbol] Color name
1103
+ def element_color(type)
1104
+ case type
1105
+ when "entity"
1106
+ :green
1107
+ when "type"
1108
+ :yellow
1109
+ when "attribute", "derived_attribute", "inverse_attribute", "parameter"
1110
+ :cyan
1111
+ when "function", "procedure"
1112
+ :magenta
1113
+ when "rule"
1114
+ :red
1115
+ else
1116
+ :white
1117
+ end
1118
+ end
1119
+
1120
+ # Check if type should be included based on filter
1121
+ # @param type [String] Element type to check
1122
+ # @return [Boolean] Whether to include
1123
+ def should_include_type?(type)
1124
+ return true unless options[:type]
1125
+
1126
+ # Handle attribute variations
1127
+ return true if options[:type] == "attribute" && type.end_with?("_attribute")
1128
+
1129
+ options[:type] == type
1130
+ end
1131
+
1132
+ # Check if all attributes should be shown
1133
+ # @return [Boolean] Whether to show all attributes
1134
+ def should_show_all_attributes?
1135
+ # Show all if specifically filtering for attributes
1136
+ options[:type]&.include?("attribute")
1137
+ end
1138
+
1139
+ # Pluralize word based on count
1140
+ # @param word [String] Word to pluralize
1141
+ # @param count [Integer] Count
1142
+ # @return [String] Singular or plural form
1143
+ def pluralize(word, count)
1144
+ count == 1 ? word : "#{word}s"
1145
+ end
1146
+
1147
+ # Output validation results in text format
1148
+ # @param validation [Hash] Validation results
1149
+ # @return [void]
1150
+ def output_text_validation(validation)
1151
+ if validation[:valid?]
1152
+ say "✓ Package is valid", :green
1153
+ else
1154
+ say "✗ Validation failed", :red
1155
+ say ""
1156
+ errors = validation[:errors] || []
1157
+ say "Errors (#{errors.size}):", :bold
1158
+ errors.each_with_index do |e, i|
1159
+ error_msg = if e.is_a?(Hash)
1160
+ # Format hash errors properly
1161
+ msg = e[:message] || "Unknown error"
1162
+ type = e[:type] ? "[#{e[:type]}] " : ""
1163
+ "#{type}#{msg}"
1164
+ else
1165
+ # Fallback for string errors
1166
+ e.to_s
1167
+ end
1168
+ say " #{i + 1}. #{error_msg}", :red
1169
+ if e.is_a?(Hash) && e[:schema]
1170
+ say " Schema: #{e[:schema]}",
1171
+ :red
1172
+ end
1173
+ if e[:referenced_schema]
1174
+ say " Referenced: #{e[:referenced_schema]}",
1175
+ :red
1176
+ end
1177
+ if e[:interface_type]
1178
+ say " Interface: #{e[:interface_type]}", :red
1179
+ end
1180
+ say " Item: #{e[:item]}", :red if e[:item]
1181
+ say " Fix: #{e[:fix_suggestion]}", :yellow if e[:fix_suggestion]
1182
+ say "" unless i == errors.size - 1
1183
+ end
1184
+ end
1185
+
1186
+ warnings = validation[:warnings]
1187
+ if warnings&.any?
1188
+ say ""
1189
+ say "Warnings (#{warnings.size}):", :bold
1190
+ warnings.each_with_index do |w, i|
1191
+ say " #{i + 1}. #{w[:message]}", :yellow
1192
+ say " Schema: #{w[:schema]}", :yellow if w[:schema]
1193
+ if w[:fix_suggestion]
1194
+ say " Fix: #{w[:fix_suggestion]}", :yellow
1195
+ end
1196
+ say "" unless i == warnings.size - 1
1197
+ end
1198
+ end
1199
+
1200
+ # Show detailed interface report if available
1201
+ if validation[:interface_report]
1202
+ say ""
1203
+ say validation[:interface_report]
1204
+ end
1205
+ end
1206
+
1207
+ # Output validation results in JSON format
1208
+ # @param validation [Hash] Validation results
1209
+ # @return [void]
1210
+ def output_json_validation(validation)
1211
+ say validation.to_json
1212
+ end
1213
+
1214
+ # Output validation results in YAML format
1215
+ # @param validation [Hash] Validation results
1216
+ # @return [void]
1217
+ def output_yaml_validation(validation)
1218
+ say validation.to_yaml
1219
+ end
1220
+
1221
+ # Output list results in text format
1222
+ # @param results [Array<Hash>] List results
1223
+ # @param type [String] Element type
1224
+ # @param schema_filter [String, nil] Schema filter
1225
+ # @param category_filter [String, nil] Category filter
1226
+ # @return [void]
1227
+ def output_text_list(results, type, schema_filter, category_filter)
1228
+ filters = []
1229
+ filters << "type: #{type}"
1230
+ filters << "schema: #{schema_filter}" if schema_filter
1231
+ filters << "category: #{category_filter}" if category_filter
1232
+ filter_text = filters.join(", ")
1233
+
1234
+ say "Elements (#{filter_text})", :bold
1235
+ say "Total: #{results.size}"
1236
+ say "=" * 60
1237
+
1238
+ results.each do |elem|
1239
+ if options[:show_path] && elem[:path]
1240
+ say elem[:path]
1241
+ elsif elem[:schema]
1242
+ category_label = elem[:category] ? " [#{elem[:category]}]" : ""
1243
+ say "#{elem[:schema]}.#{elem[:id]}#{category_label}"
1244
+ else
1245
+ say elem[:id] || "(unnamed)"
1246
+ end
1247
+ end
1248
+ end
1249
+
1250
+ # Output search results in text format
1251
+ # @param results [Array<Hash>] Search results
1252
+ # @param pattern [String] Search pattern
1253
+ # @return [void]
1254
+ def output_text_search_results(results, pattern)
1255
+ say "Search Results for: #{pattern}", :bold
1256
+ say "Total: #{results.size}"
1257
+ say "=" * 60
1258
+
1259
+ results.each do |elem|
1260
+ type_label = "[#{elem[:type]}]"
1261
+
1262
+ if options[:show_path] && elem[:path]
1263
+ say "#{type_label.ljust(20)} #{elem[:path]}"
1264
+ elsif elem[:schema]
1265
+ category_label = elem[:category] ? " [#{elem[:category]}]" : ""
1266
+ say "#{type_label.ljust(20)} #{elem[:schema]}.#{elem[:id]}#{category_label}"
1267
+ else
1268
+ say "#{type_label.ljust(20)} #{elem[:id] || '(unnamed)'}"
1269
+ end
1270
+ end
1271
+ end
1272
+ end
1273
+ end
1274
+ end