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
@@ -1,6 +1,10 @@
1
+ require_relative "formatters/remark_item_formatter"
2
+
1
3
  module Expressir
2
4
  module Express
3
5
  class Formatter
6
+ include RemarkItemFormatter
7
+
4
8
  INDENT_CHAR = " ".freeze
5
9
  INDENT_WIDTH = 2
6
10
  INDENT = INDENT_CHAR * INDENT_WIDTH
@@ -194,7 +198,7 @@ module Expressir
194
198
  when Model::Declarations::InterfacedItem
195
199
  # not implemented yet
196
200
  when Model::Declarations::RemarkItem
197
- # not implemented yet
201
+ format_remark_item(node)
198
202
  when Model::Cache
199
203
  # not implemented yet
200
204
  when Model::ModelElement
@@ -0,0 +1,25 @@
1
+ module Expressir
2
+ module Express
3
+ # Formatter for RemarkItem declarations
4
+ module RemarkItemFormatter
5
+ # Format a RemarkItem as an EXPRESS remark
6
+ # @param node [Model::Declarations::RemarkItem] The remark item to format
7
+ # @return [String] Formatted remark
8
+ def format_remark_item(node)
9
+ return "" unless node.remarks&.any?
10
+
11
+ # Check if any remark contains newlines
12
+ has_newlines = node.remarks.any? { |r| r.to_s.include?("\n") }
13
+
14
+ if has_newlines
15
+ # Multi-line format: (*"path" remarks *)
16
+ remarks_text = node.remarks.join("\n")
17
+ "(*\"#{node.path}\"\n#{remarks_text}\n*)"
18
+ else
19
+ # Single-line format: --"path"
20
+ "--\"#{node.path}\""
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -680,6 +680,39 @@ root_path: nil)
680
680
 
681
681
  @repository
682
682
  end
683
+
684
+ # Parses Express content string into an Express model
685
+ # @param [String] content Express content as string
686
+ # @param [Boolean] skip_references skip resolving references
687
+ # @param [Boolean] include_source attach original source code to model elements
688
+ # @return [Model::Repository]
689
+ # @raise [SchemaParseFailure] if the content fails to parse
690
+ def self.from_exp(content, skip_references: nil, include_source: nil)
691
+ begin
692
+ ast = Parser.new.parse content
693
+ rescue Parslet::ParseFailed => e
694
+ raise Error::SchemaParseFailure.new("(from string)", e)
695
+ end
696
+
697
+ visitor = Expressir::Express::Visitor.new(content,
698
+ include_source: include_source)
699
+ repository = visitor.visit_ast ast, :top
700
+
701
+ repository.schemas.each do |schema|
702
+ schema.file = nil
703
+ schema.file_basename = nil
704
+ schema.formatted = schema.to_s(no_remarks: true)
705
+ end
706
+
707
+ unless skip_references
708
+ Expressir::Benchmark.measure_references do
709
+ resolve_references_model_visitor = ResolveReferencesModelVisitor.new
710
+ resolve_references_model_visitor.visit(repository)
711
+ end
712
+ end
713
+
714
+ repository
715
+ end
683
716
  end
684
717
  end
685
718
  end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../model/dependency_resolver"
4
+
5
+ module Expressir
6
+ module Manifest
7
+ # Resolves schema paths in manifests using pattern-based path discovery
8
+ # Attempts to find file paths for schemas with missing or empty paths
9
+ class Resolver
10
+ attr_reader :manifest, :options, :base_dirs
11
+
12
+ # Initialize resolver
13
+ # @param manifest [SchemaManifest] The manifest to resolve
14
+ # @param options [Hash] Resolution options
15
+ # @option options [Array<String>] :base_dirs Base directories to search
16
+ # @option options [Boolean] :verbose Enable verbose output
17
+ def initialize(manifest, options = {})
18
+ @manifest = manifest
19
+ @options = options
20
+ @base_dirs = extract_base_dirs_from_manifest
21
+
22
+ # Override with explicit base_dirs if provided
23
+ if options[:base_dirs]
24
+ @base_dirs = Array(options[:base_dirs]).map do |d|
25
+ File.expand_path(d)
26
+ end
27
+ end
28
+ end
29
+
30
+ # Resolve paths for all schemas in manifest
31
+ # Works like manifest create - resolves all dependencies from schemas with paths
32
+ # @return [SchemaManifest] New manifest with resolved paths and dependencies
33
+ def resolve_paths
34
+ # Create new manifest to avoid modifying original
35
+ resolved_manifest = SchemaManifest.new
36
+
37
+ # Track all resolved schema paths
38
+ all_resolved_paths = Set.new
39
+ all_unresolved = []
40
+
41
+ # Find schemas with valid paths to use as roots
42
+ root_schemas = manifest.schemas.select do |s|
43
+ !s.path.nil? && !s.path.empty? && File.exist?(s.path)
44
+ end
45
+
46
+ if root_schemas.empty?
47
+ # No roots, just try to resolve individual schemas
48
+ say "No valid schema paths found, attempting simple path resolution..." if options[:verbose]
49
+ return resolve_paths_simple
50
+ end
51
+
52
+ say "Resolving dependencies from #{root_schemas.size} root schema(s)..." if options[:verbose]
53
+ say_base_dirs
54
+
55
+ # For each root schema, resolve all its dependencies
56
+ root_schemas.each do |root_entry|
57
+ resolver = Model::DependencyResolver.new(
58
+ base_dirs: @base_dirs,
59
+ schema_registry: build_schema_registry,
60
+ verbose: options[:verbose],
61
+ )
62
+
63
+ # Resolve all dependencies for this root
64
+ resolved_paths = resolver.resolve_dependencies(root_entry.path)
65
+ all_resolved_paths.merge(resolved_paths)
66
+ all_unresolved.concat(resolver.unresolved)
67
+ end
68
+
69
+ # Add all resolved schemas to manifest
70
+ all_resolved_paths.each do |path|
71
+ schema_id = extract_schema_name(path)
72
+ resolved_manifest.schemas << SchemaManifestEntry.new(
73
+ id: schema_id,
74
+ path: path,
75
+ )
76
+ end
77
+
78
+ # Add unresolved schemas with empty paths
79
+ all_unresolved.uniq { |e| e[:schema_name] }.each do |entry|
80
+ # Only add if not already in manifest
81
+ unless resolved_manifest.schemas.any? do |s|
82
+ s.id == entry[:schema_name]
83
+ end
84
+ resolved_manifest.schemas << SchemaManifestEntry.new(
85
+ id: entry[:schema_name],
86
+ path: "",
87
+ )
88
+ end
89
+ end
90
+
91
+ resolved_manifest
92
+ end
93
+
94
+ # Simple path resolution - just try to find missing paths
95
+ # Used when no valid root schemas are available
96
+ # @return [SchemaManifest] New manifest with resolved paths
97
+ def resolve_paths_simple
98
+ resolved_manifest = SchemaManifest.new
99
+
100
+ resolver = Model::DependencyResolver.new(
101
+ base_dirs: @base_dirs,
102
+ schema_registry: build_schema_registry,
103
+ verbose: options[:verbose],
104
+ )
105
+
106
+ manifest.schemas.each do |schema_entry|
107
+ resolved_entry = resolve_schema_entry(schema_entry, resolver)
108
+ resolved_manifest.schemas << resolved_entry
109
+ end
110
+
111
+ resolved_manifest
112
+ end
113
+
114
+ # Get resolution statistics
115
+ # @return [Hash] Resolution statistics
116
+ def statistics
117
+ unresolved_count = manifest.schemas.count do |s|
118
+ s.path.nil? || s.path.empty?
119
+ end
120
+ {
121
+ total_schemas: manifest.schemas.size,
122
+ resolved: manifest.schemas.size - unresolved_count,
123
+ unresolved: unresolved_count,
124
+ base_dirs: @base_dirs,
125
+ }
126
+ end
127
+
128
+ private
129
+
130
+ # Extract the actual schema name from a schema file by parsing it
131
+ # @param schema_path [String] Path to the schema file
132
+ # @return [String] The schema name declared in the file
133
+ def extract_schema_name(schema_path)
134
+ require_relative "../express/parser"
135
+ repo = Expressir::Express::Parser.from_file(schema_path)
136
+ schema = repo.schemas.first
137
+ schema&.id || File.basename(schema_path, ".*")
138
+ rescue StandardError
139
+ # Fallback to filename if parsing fails
140
+ File.basename(schema_path, ".*")
141
+ end
142
+
143
+ # Say message (only used when verbose is enabled)
144
+ def say(message)
145
+ puts message if options[:verbose]
146
+ end
147
+
148
+ # Say base directories with numbered labels
149
+ def say_base_dirs
150
+ if options[:verbose] && @base_dirs
151
+ say " Using base directories:"
152
+ @base_dirs.each_with_index do |dir, index|
153
+ say " - [source #{index + 1}]: #{dir}"
154
+ end
155
+ end
156
+ end
157
+
158
+ # Resolve a single schema entry
159
+ # @param schema_entry [SchemaManifestEntry] The entry to resolve
160
+ # @param resolver [Model::DependencyResolver] The resolver to use
161
+ # @return [SchemaManifestEntry] New entry with resolved path
162
+ def resolve_schema_entry(schema_entry, resolver)
163
+ # If path already exists and is valid, keep it
164
+ if schema_entry.path && !schema_entry.path.empty? && File.exist?(schema_entry.path)
165
+ return SchemaManifestEntry.new(
166
+ id: schema_entry.id,
167
+ path: schema_entry.path,
168
+ )
169
+ end
170
+
171
+ # Try to resolve path using DependencyResolver
172
+ resolved_path = resolver.resolve_schema_location(
173
+ schema_entry.id,
174
+ "USE", # Kind doesn't matter for location resolution
175
+ nil, # No current path context
176
+ )
177
+
178
+ SchemaManifestEntry.new(
179
+ id: schema_entry.id,
180
+ path: resolved_path || schema_entry.path || "",
181
+ )
182
+ end
183
+
184
+ # Build schema registry from manifest entries with valid paths
185
+ # Maps schema IDs to their file paths
186
+ # @return [Hash<String, String>] Schema name => path mapping
187
+ def build_schema_registry
188
+ registry = {}
189
+ manifest.schemas.each do |schema_entry|
190
+ next if schema_entry.id.nil?
191
+ next if schema_entry.path.nil? || schema_entry.path.empty?
192
+ next unless File.exist?(schema_entry.path)
193
+
194
+ registry[schema_entry.id] = schema_entry.path
195
+ end
196
+ registry
197
+ end
198
+
199
+ # Extract base directories from manifest schema paths
200
+ # @return [Array<String>] List of unique base directories
201
+ def extract_base_dirs_from_manifest
202
+ dirs = manifest.schemas
203
+ .reject { |s| s.path.nil? || s.path.empty? }
204
+ .select { |s| File.exist?(s.path) }
205
+ .map { |s| File.dirname(File.expand_path(s.path)) }
206
+ .uniq
207
+
208
+ # If no directories found, use current directory
209
+ dirs.empty? ? [Dir.pwd] : dirs
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../model/dependency_resolver"
4
+
5
+ module Expressir
6
+ module Manifest
7
+ # Validates schema manifests for completeness and integrity
8
+ # Performs three types of validation:
9
+ # 1. File existence - checks if schema files exist
10
+ # 2. Path completeness - warns about missing paths
11
+ # 3. Referential integrity - validates USE/REFERENCE FROM resolution
12
+ class Validator
13
+ attr_reader :manifest, :options
14
+
15
+ # Initialize validator
16
+ # @param manifest [SchemaManifest] The manifest to validate
17
+ # @param options [Hash] Validation options
18
+ # @option options [Boolean] :verbose Enable verbose output
19
+ def initialize(manifest, options = {})
20
+ @manifest = manifest
21
+ @options = options
22
+ @base_dirs = extract_base_dirs_from_manifest
23
+ end
24
+
25
+ # Validate that all schema files exist on disk
26
+ # @return [Array<Hash>] Array of error hashes with :schema, :path, :message
27
+ def validate_file_existence
28
+ errors = []
29
+ manifest.schemas.each do |schema_entry|
30
+ next if schema_entry.path.nil? || schema_entry.path.empty?
31
+
32
+ unless File.exist?(schema_entry.path)
33
+ errors << {
34
+ schema: schema_entry.id,
35
+ path: schema_entry.path,
36
+ message: "Schema file not found: #{schema_entry.path} (#{schema_entry.id})",
37
+ }
38
+ end
39
+ end
40
+ errors
41
+ end
42
+
43
+ # Validate that all schemas have paths specified
44
+ # @return [Array<Hash>] Array of warning hashes with :schema, :message
45
+ def validate_path_completeness
46
+ warnings = []
47
+ manifest.schemas.each do |schema_entry|
48
+ if schema_entry.path.nil? || schema_entry.path.empty?
49
+ warnings << {
50
+ schema: schema_entry.id,
51
+ message: "Schema '#{schema_entry.id}' has no path specified - please provide path",
52
+ }
53
+ end
54
+ end
55
+ warnings
56
+ end
57
+
58
+ # Validate that manifest IDs match actual schema names in files
59
+ # @return [Array<Hash>] Array of error hashes with :schema, :path, :actual_name, :message
60
+ def validate_schema_names
61
+ errors = []
62
+ manifest.schemas.each do |schema_entry|
63
+ next if schema_entry.path.nil? || schema_entry.path.empty?
64
+ next unless File.exist?(schema_entry.path)
65
+
66
+ begin
67
+ actual_name = extract_schema_name(schema_entry.path)
68
+ if actual_name != schema_entry.id
69
+ errors << {
70
+ schema: schema_entry.id,
71
+ path: schema_entry.path,
72
+ actual_name: actual_name,
73
+ message: "Schema ID mismatch: manifest has '#{schema_entry.id}' but file declares '#{actual_name}'",
74
+ }
75
+ end
76
+ rescue StandardError
77
+ # Skip validation if we can't parse the file
78
+ next
79
+ end
80
+ end
81
+ errors
82
+ end
83
+
84
+ # Validate referential integrity using DependencyResolver
85
+ # Checks that all USE FROM and REFERENCE FROM interfaces can be resolved
86
+ # @return [Array<Hash>] Array of error hashes with interface details
87
+ def validate_referential_integrity
88
+ errors = []
89
+
90
+ # Filter schemas to validate
91
+ schemas_to_validate = manifest.schemas.select do |s|
92
+ !s.path.nil? && !s.path.empty? && File.exist?(s.path)
93
+ end
94
+
95
+ # Show header if verbose
96
+ if @options[:verbose]
97
+ puts "Validating referential integrity for #{schemas_to_validate.size} schemas..."
98
+ puts ""
99
+ end
100
+
101
+ # Create resolver ONCE for all schemas (efficiency)
102
+ # Share verbose output behavior with manifest create command (DRY)
103
+ resolver = Model::DependencyResolver.new(
104
+ base_dirs: @base_dirs,
105
+ schema_registry: build_schema_registry,
106
+ verbose: @options[:verbose], # Let resolver show detailed interface resolution
107
+ )
108
+
109
+ schemas_to_validate.each do |schema_entry|
110
+ # Extract interfaces from this schema
111
+ begin
112
+ interfaces = resolver.extract_interfaces(schema_entry.path)
113
+ rescue StandardError => e
114
+ errors << {
115
+ schema: schema_entry.id,
116
+ path: schema_entry.path,
117
+ message: "Failed to parse schema: #{e.message}",
118
+ }
119
+ next
120
+ end
121
+
122
+ # Check each interface can be resolved
123
+ # The resolver will print verbose output for each resolution when verbose=true
124
+ interfaces.each do |interface|
125
+ # Print schema name context if verbose (matches create command style)
126
+ if @options[:verbose]
127
+ print " #{interface[:kind]} FROM #{interface[:schema_name]} (in #{schema_entry.id}): "
128
+ end
129
+
130
+ resolved_path = resolver.resolve_schema_location(
131
+ interface[:schema_name],
132
+ interface[:kind],
133
+ schema_entry.path,
134
+ )
135
+
136
+ if resolved_path.nil?
137
+ puts "\e[31m✗ not found\e[0m" if @options[:verbose]
138
+
139
+ errors << {
140
+ schema: schema_entry.id,
141
+ path: schema_entry.path,
142
+ interface_kind: interface[:kind],
143
+ referenced_schema: interface[:schema_name],
144
+ message: "Cannot resolve #{interface[:kind]} FROM #{interface[:schema_name]} in #{schema_entry.id}",
145
+ }
146
+ elsif @options[:verbose]
147
+ relative_path = File.basename(resolved_path)
148
+ puts "\e[32m✓\e[0m #{relative_path}"
149
+ end
150
+ end
151
+ end
152
+
153
+ errors
154
+ end
155
+
156
+ private
157
+
158
+ # Extract the actual schema name from a schema file by parsing it
159
+ # @param schema_path [String] Path to the schema file
160
+ # @return [String] The schema name declared in the file
161
+ def extract_schema_name(schema_path)
162
+ require_relative "../express/parser"
163
+ repo = Expressir::Express::Parser.from_file(schema_path)
164
+ schema = repo.schemas.first
165
+ schema&.id || File.basename(schema_path, ".*")
166
+ end
167
+
168
+ # Build schema registry from manifest entries
169
+ # Maps schema IDs to their file paths
170
+ # @return [Hash<String, String>] Schema name => path mapping
171
+ def build_schema_registry
172
+ registry = {}
173
+ manifest.schemas.each do |schema_entry|
174
+ next if schema_entry.id.nil?
175
+ next if schema_entry.path.nil? || schema_entry.path.empty?
176
+
177
+ registry[schema_entry.id] = schema_entry.path
178
+ end
179
+ registry
180
+ end
181
+
182
+ # Extract base directories from manifest schema paths
183
+ # @return [Array<String>] List of unique base directories
184
+ def extract_base_dirs_from_manifest
185
+ dirs = manifest.schemas
186
+ .reject { |s| s.path.nil? || s.path.empty? }
187
+ .map { |s| File.dirname(File.expand_path(s.path)) }
188
+ .uniq
189
+
190
+ # If no directories found, use current directory
191
+ dirs.empty? ? [Dir.pwd] : dirs
192
+ end
193
+ end
194
+ end
195
+ end
@@ -10,6 +10,8 @@ module Expressir
10
10
  attribute :supertype_expression, ModelElement
11
11
  attribute :subtype_of, ModelElement, collection: true
12
12
  attribute :attributes, Attribute, collection: true
13
+ attribute :derived_attributes, DerivedAttribute, collection: true
14
+ attribute :inverse_attributes, InverseAttribute, collection: true
13
15
  attribute :unique_rules, UniqueRule, collection: true
14
16
  attribute :where_rules, WhereRule, collection: true
15
17
  attribute :informal_propositions, InformalPropositionRule,
@@ -22,6 +24,8 @@ module Expressir
22
24
  map "supertype_expression", to: :supertype_expression
23
25
  map "subtype_of", to: :subtype_of
24
26
  map "attributes", to: :attributes
27
+ map "derived_attributes", to: :derived_attributes
28
+ map "inverse_attributes", to: :inverse_attributes
25
29
  map "unique_rules", to: :unique_rules
26
30
  map "where_rules", to: :where_rules
27
31
  map "informal_propositions", to: :informal_propositions
@@ -31,6 +35,8 @@ module Expressir
31
35
  def children
32
36
  [
33
37
  *attributes,
38
+ *derived_attributes,
39
+ *inverse_attributes,
34
40
  *unique_rules,
35
41
  *where_rules,
36
42
  *informal_propositions,