expressir 2.1.29 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +98 -0
- data/.github/workflows/links.yml +100 -0
- data/.github/workflows/rake.yml +4 -0
- data/.github/workflows/release.yml +5 -0
- data/.github/workflows/validate_schemas.yml +1 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +209 -55
- data/Gemfile +2 -1
- data/README.adoc +650 -83
- data/docs/Gemfile +12 -0
- data/docs/_config.yml +141 -0
- data/docs/_guides/changes/changes-format.adoc +778 -0
- data/docs/_guides/changes/importing-eengine.adoc +898 -0
- data/docs/_guides/changes/index.adoc +396 -0
- data/docs/_guides/changes/programmatic-usage.adoc +1038 -0
- data/docs/_guides/changes/validating-changes.adoc +681 -0
- data/docs/_guides/cli/benchmark-performance.adoc +834 -0
- data/docs/_guides/cli/coverage-analysis.adoc +921 -0
- data/docs/_guides/cli/format-schemas.adoc +547 -0
- data/docs/_guides/cli/index.adoc +8 -0
- data/docs/_guides/cli/managing-changes.adoc +927 -0
- data/docs/_guides/cli/validate-ascii.adoc +645 -0
- data/docs/_guides/cli/validate-schemas.adoc +534 -0
- data/docs/_guides/index.adoc +165 -0
- data/docs/_guides/ler/creating-packages.adoc +664 -0
- data/docs/_guides/ler/index.adoc +305 -0
- data/docs/_guides/ler/loading-packages.adoc +707 -0
- data/docs/_guides/ler/package-formats.adoc +748 -0
- data/docs/_guides/ler/querying-packages.adoc +826 -0
- data/docs/_guides/ler/validating-packages.adoc +750 -0
- data/docs/_guides/liquid/basic-templates.adoc +813 -0
- data/docs/_guides/liquid/documentation-generation.adoc +1042 -0
- data/docs/_guides/liquid/drops-reference.adoc +829 -0
- data/docs/_guides/liquid/filters-and-tags.adoc +912 -0
- data/docs/_guides/liquid/index.adoc +468 -0
- data/docs/_guides/manifests/creating-manifests.adoc +483 -0
- data/docs/_guides/manifests/index.adoc +307 -0
- data/docs/_guides/manifests/resolving-manifests.adoc +557 -0
- data/docs/_guides/manifests/validating-manifests.adoc +713 -0
- data/docs/_guides/ruby-api/formatting-schemas.adoc +605 -0
- data/docs/_guides/ruby-api/index.adoc +257 -0
- data/docs/_guides/ruby-api/parsing-files.adoc +421 -0
- data/docs/_guides/ruby-api/search-engine.adoc +609 -0
- data/docs/_guides/ruby-api/working-with-repository.adoc +577 -0
- data/docs/_pages/data-model.adoc +665 -0
- data/docs/_pages/express-language.adoc +506 -0
- data/docs/_pages/getting-started.adoc +414 -0
- data/docs/_pages/index.adoc +116 -0
- data/docs/_pages/introduction.adoc +256 -0
- data/docs/_pages/ler-packages.adoc +837 -0
- data/docs/_pages/parsers.adoc +683 -0
- data/docs/_pages/schema-manifests.adoc +431 -0
- data/docs/_references/index.adoc +228 -0
- data/docs/_tutorials/creating-ler-package.adoc +735 -0
- data/docs/_tutorials/documentation-coverage.adoc +795 -0
- data/docs/_tutorials/index.adoc +221 -0
- data/docs/_tutorials/liquid-templates.adoc +806 -0
- data/docs/_tutorials/parsing-your-first-schema.adoc +522 -0
- data/docs/_tutorials/querying-schemas.adoc +751 -0
- data/docs/_tutorials/working-with-multiple-schemas.adoc +676 -0
- data/docs/index.adoc +242 -0
- data/docs/lychee.toml +84 -0
- data/examples/demo_ler_usage.sh +86 -0
- data/examples/ler/README.md +111 -0
- data/examples/ler/simple_example.ler +0 -0
- data/examples/ler/simple_schema.exp +33 -0
- data/examples/ler_build.rb +75 -0
- data/examples/ler_cli.rb +79 -0
- data/examples/ler_demo_complete.rb +276 -0
- data/examples/ler_query.rb +91 -0
- data/examples/ler_query_examples.rb +305 -0
- data/examples/ler_stats.rb +81 -0
- data/examples/phase3_demo.rb +159 -0
- data/examples/query_demo_simple.rb +131 -0
- data/expressir.gemspec +2 -0
- data/lib/expressir/changes/schema_change.rb +32 -22
- data/lib/expressir/changes/{edition_change.rb → version_change.rb} +3 -3
- data/lib/expressir/cli.rb +12 -4
- data/lib/expressir/commands/changes_import_eengine.rb +2 -2
- data/lib/expressir/commands/changes_validate.rb +1 -1
- data/lib/expressir/commands/manifest.rb +427 -0
- data/lib/expressir/commands/package.rb +1274 -0
- data/lib/expressir/commands/validate.rb +70 -37
- data/lib/expressir/commands/validate_ascii.rb +607 -0
- data/lib/expressir/commands/validate_load.rb +88 -0
- data/lib/expressir/express/formatter.rb +5 -1
- data/lib/expressir/express/formatters/remark_item_formatter.rb +25 -0
- data/lib/expressir/express/parser.rb +33 -0
- data/lib/expressir/manifest/resolver.rb +213 -0
- data/lib/expressir/manifest/validator.rb +195 -0
- data/lib/expressir/model/declarations/entity.rb +6 -0
- data/lib/expressir/model/dependency_resolver.rb +270 -0
- data/lib/expressir/model/indexes/entity_index.rb +103 -0
- data/lib/expressir/model/indexes/reference_index.rb +148 -0
- data/lib/expressir/model/indexes/type_index.rb +149 -0
- data/lib/expressir/model/interface_validator.rb +384 -0
- data/lib/expressir/model/repository.rb +400 -5
- data/lib/expressir/model/repository_validator.rb +295 -0
- data/lib/expressir/model/search_engine.rb +525 -0
- data/lib/expressir/model.rb +4 -94
- data/lib/expressir/package/builder.rb +200 -0
- data/lib/expressir/package/metadata.rb +81 -0
- data/lib/expressir/package/reader.rb +165 -0
- data/lib/expressir/schema_manifest.rb +11 -1
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +16 -3
- metadata +115 -5
- data/docs/benchmarking.adoc +0 -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
|