mxrb 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 571786ae22b838daaa4700d7a99816653046f645a5d4f49ee29d155f4a05fa99
4
- data.tar.gz: 4366822b4731a3604eb2be09ee1f4408fe93108229486b6b45528772022c709f
3
+ metadata.gz: a734dae7c32bec492facdf80e775897c3b31af47fc595f4d4096f2cd27f091ee
4
+ data.tar.gz: db3a5207301da5c8f384515adf334c5739fba69379aec3c69175a653e68beff2
5
5
  SHA512:
6
- metadata.gz: 97f38f00ed166946e0ea54e30c940631c2dfd91d919a20c9b701e8df25acd0cb653a68ae6f0bee8eb0742333ec1c921fb523882200be48def5a3a827bfe82e99
7
- data.tar.gz: 648bd2c542071702fb6e9ac44559bfac32dd6128c0bb666f24c0f058992db702a69be6815db9bbd9093d5e92928c836d61e4cb84e28235d2dbf4742a1acac3f3
6
+ metadata.gz: 4aedde89b67e5d52e438b38e2f2c9835a9ca2407950a6c7ee04f10aa74fb0d1e9fae0fc380b50e3c3155fc7509eb0d513e90461c8058949849f9c4146df162c7
7
+ data.tar.gz: ed923449e412dc172462c951373408558244983db0d84b38d2a550791219190146064d900e2b6eb459fc8e6aa250def2c62614854e1e8b25d13192a6b437bd6e
data/bin/mxrb CHANGED
@@ -64,7 +64,9 @@ end
64
64
 
65
65
  SCAFFOLD_COMMANDS = (Mxrb::Scaffold::Help::COMMANDS.keys - ["design"]).freeze
66
66
 
67
+ progress_disabled = ARGV.delete("--no-progress")
67
68
  command = ARGV.shift
69
+ Mxrb::Progress.configure(enabled: false) if progress_disabled || ARGV.include?("--json")
68
70
 
69
71
  case command
70
72
  when '--version', '-v'
@@ -1506,6 +1508,7 @@ else
1506
1508
  mxrb — Pure-Ruby Mendix .mpr toolkit
1507
1509
 
1508
1510
  Commands:
1511
+ --no-progress Disable interactive progress rendering
1509
1512
  --version, -v Show the installed MXRB version
1510
1513
  doctor [DIR] [--json] Check project files and local toolchain
1511
1514
  benchmark <file.mpr> Measure open, index and validation time
@@ -94,6 +94,12 @@ entity :AnimalSearchResult do
94
94
  string :Name
95
95
  end
96
96
 
97
+ entity :AnimalSummaryView do
98
+ oql_view source: "VetClinic.AnimalSummarySource"
99
+ string :Name
100
+ integer :Total
101
+ end
102
+
97
103
  entity :Animal do
98
104
  before_commit microflow: "VetClinic.VAL_Animal"
99
105
  after_commit microflow: "VetClinic.ACT_AfterAnimalCommit"
@@ -111,6 +117,8 @@ end
111
117
 
112
118
  Em `access_rule`, `read:` e `write:` aceitam `:all`, `:none` ou uma lista de
113
119
  atributos. Declare um ou mais papéis qualificados como primeiros argumentos.
120
+ `oql_view` liga a entidade ao `ViewEntitySourceDocument`; ao exportar um projeto
121
+ existente, a consulta OQL aparece junto da entidade em `domain/oql_views`.
114
122
 
115
123
  Use `mxrb entity --help` para o comando e `mxrb generate project.rb` seguido de
116
124
  `mxrb validate App.mpr` para validar o resultado.
@@ -5,6 +5,12 @@
5
5
  `mxrb generate` avalia uma definição Ruby e cria ou atualiza o MPR. Nomes
6
6
  Mendix são chaves estáveis, portanto reaplicar a definição não duplica units.
7
7
 
8
+ Operações demoradas exibem progresso em `stderr` quando executadas em um
9
+ terminal interativo. Quando o total é conhecido, o MXRB mostra barra e
10
+ porcentagem; operações externas sem total confiável mostram spinner e tempo
11
+ decorrido. Pipes, redirecionamentos e saídas `--json` permanecem limpos. Use
12
+ `--no-progress` ou `MXRB_PROGRESS=0` para desativar explicitamente.
13
+
8
14
  Para iniciar em uma pasta vazia:
9
15
 
10
16
  ```sh
@@ -122,6 +128,28 @@ usam `bson_binary`. Alterações nesse Ruby sobrepõem o baseline antes das escr
122
128
  tipadas. Corpos de flow possuem `body_fingerprint`: o grafo nativo é reutilizado
123
129
  quando o Ruby não mudou e regenerado após uma edição.
124
130
 
131
+ Export mappings, import mappings e JSON structures são materializados por
132
+ módulo em `infrastructure/mappings/{exports,imports,json_structures}`. Esses
133
+ arquivos mantêm `UnitID` e container/pasta originais, podem editar toda a
134
+ estrutura profunda em Ruby e têm precedência sobre o baseline durante o
135
+ `generate`.
136
+
137
+ Serviços REST, OData e web services publicados são exportados para
138
+ `infrastructure/endpoints`; recursos, operações, métodos HTTP, paths,
139
+ parâmetros e referências a microflows permanecem no mesmo arquivo Ruby.
140
+ Serviços consumidos são roteados para `infrastructure/integrations`, enquanto
141
+ message definitions e XML schemas ficam em `infrastructure/mappings`.
142
+
143
+ O Domain Model exportado separa entidades persistentes em `domain/entities`,
144
+ DTOs/non-persistent em `domain/dtos` e entidades de OQL View em
145
+ `domain/oql_views`. O arquivo da OQL View também contém seu
146
+ `DomainModels$ViewEntitySourceDocument`, portanto a consulta `Oql` pode ser
147
+ editada no mesmo Ruby sem perder a referência da entidade. Datasets OQL ficam
148
+ em `application/queries/datasets`; enumerations e constants ficam em
149
+ `domain/enumerations` e `domain/constants`. Scheduled events ficam em
150
+ `application/jobs/scheduled_events`. O `domain/model.rb` e o
151
+ `application/application.rb` carregam automaticamente todas essas categorias.
152
+
125
153
  A DSL cobre criação/alteração/retrieve/commit/delete, chamadas de microflow,
126
154
  Java, JavaScript, nanoflow e app service, páginas, REST, listas, decisões,
127
155
  loops, eventos de erro e rescue.
@@ -19,16 +19,24 @@ module Mxrb
19
19
  @deployment = File.expand_path(deployment || File.join(@project_root, 'deployment'))
20
20
  end
21
21
 
22
- def pack(output:, force: false)
23
- output_path = File.expand_path(output)
24
- validate_input!(output_path, force:)
25
- version = Mxrb.open(@mpr_path, &:mendix_version)
26
- adapter = Adapter.for(version)
27
- metadata = adapter.validate_deployment!(@deployment)
28
- adapter.validate_freshness!(@mpr_path, @deployment)
29
- files, directories = inventory
30
- write_atomically(output_path, files, directories)
31
- package_result(output_path, version, metadata)
22
+ def pack(output:, force: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
23
+ Progress.with("Packing #{File.basename(@mpr_path)}") do |progress|
24
+ output_path = File.expand_path(output)
25
+ validate_input!(output_path, force:)
26
+ version = Mxrb.open(@mpr_path, &:mendix_version)
27
+ adapter = Adapter.for(version)
28
+ metadata = adapter.validate_deployment!(@deployment)
29
+ adapter.validate_freshness!(@mpr_path, @deployment)
30
+ files, directories = inventory
31
+ progress.update(
32
+ current: 3, total: files.size + directories.size + 4,
33
+ detail: "#{files.size} files"
34
+ )
35
+ write_atomically(output_path, files, directories, progress)
36
+ package_result(output_path, version, metadata).tap do
37
+ progress.advance(detail: 'package checksum')
38
+ end
39
+ end
32
40
  end
33
41
 
34
42
  private
@@ -69,13 +77,19 @@ module Mxrb
69
77
  path.delete_prefix("#{@deployment}/").tr('\\', '/')
70
78
  end
71
79
 
72
- def write_atomically(output, files, directories)
80
+ def write_atomically(output, files, directories, progress) # rubocop:disable Metrics/MethodLength
73
81
  FileUtils.mkdir_p(File.dirname(output))
74
82
  Dir.mktmpdir('mxrb-mda-', File.dirname(output)) do |tmpdir|
75
83
  temporary = File.join(tmpdir, 'package.mda')
76
84
  Zip::File.open(temporary, create: true) do |archive|
77
- directories.each { add_directory(archive, _1) }
78
- files.each { add_file(archive, _1) }
85
+ directories.each do |directory|
86
+ add_directory(archive, directory)
87
+ progress.advance(detail: relative(directory))
88
+ end
89
+ files.each do |file|
90
+ add_file(archive, file)
91
+ progress.advance(detail: relative(file))
92
+ end
79
93
  end
80
94
  FileUtils.mv(temporary, output, force: true)
81
95
  end
@@ -309,17 +309,22 @@ module Mxrb
309
309
  @mendix_home = mendix_home && File.expand_path(mendix_home)
310
310
  end
311
311
 
312
- def pack(output:, force: false)
313
- output_path = File.expand_path(output)
314
- validate_output!(output_path, force:)
315
- version = Mxrb.open(@mpr_path, &:mendix_version)
316
- adapter = Adapter.for(version)
317
- metadata = adapter.validate_deployment!(@deployment)
318
- adapter.validate_freshness!(@mpr_path, @deployment)
319
- runtime = runtime_root(version)
320
- validate_runtime!(runtime)
321
- PortableArchiveWriter.new(@deployment, runtime, metadata).write(output_path)
322
- result(output_path, version, metadata)
312
+ def pack(output:, force: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
313
+ Progress.with("Packing portable Runtime for #{File.basename(@mpr_path)}") do |progress|
314
+ output_path = File.expand_path(output)
315
+ validate_output!(output_path, force:)
316
+ progress.update(detail: 'validating deployment')
317
+ version = Mxrb.open(@mpr_path, &:mendix_version)
318
+ adapter = Adapter.for(version)
319
+ metadata = adapter.validate_deployment!(@deployment)
320
+ adapter.validate_freshness!(@mpr_path, @deployment)
321
+ runtime = runtime_root(version)
322
+ validate_runtime!(runtime)
323
+ progress.update(detail: 'writing Runtime archive')
324
+ PortableArchiveWriter.new(@deployment, runtime, metadata).write(output_path)
325
+ progress.update(detail: 'calculating checksum')
326
+ result(output_path, version, metadata)
327
+ end
323
328
  end
324
329
 
325
330
  private
@@ -872,15 +872,21 @@ module Mxrb
872
872
  @scheduled_events << sb.to_h
873
873
  end
874
874
 
875
- def native_document(name, type:, deep_structure:, containment: 'Documents')
875
+ def native_document(name, type:, deep_structure:, containment: 'Documents',
876
+ unit_id: nil, container_id: nil)
876
877
  raise ArgumentError, 'deep_structure requires a Hash' unless deep_structure.is_a?(Hash)
877
878
 
878
879
  @native_documents << {
879
880
  name: name.to_s, type: type.to_s, containment: containment.to_s,
881
+ unit_id: unit_id&.to_s, container_id: container_id&.to_s,
880
882
  doc: { '$Type' => type.to_s, 'Name' => name.to_s }.merge(deep_structure)
881
883
  }
882
884
  end
883
885
 
886
+ def bson_binary(base64, subtype: :generic)
887
+ BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
888
+ end
889
+
884
890
  # Native responsive application shell. It deliberately uses only Mendix
885
891
  # core widgets, so generated projects do not depend on Atlas Core merely
886
892
  # to display their navigation.
@@ -1070,6 +1076,7 @@ module Mxrb
1070
1076
  @generalization = nil
1071
1077
  @system_members = nil
1072
1078
  @indexes = nil
1079
+ @oql_view = nil
1073
1080
  end
1074
1081
 
1075
1082
  ATTR_TYPES.each do |type|
@@ -1081,6 +1088,16 @@ module Mxrb
1081
1088
  def non_persistent! = (@persistable = false)
1082
1089
  def documentation(d) = (@doc = d)
1083
1090
 
1091
+ # Marks an entity as an OQL-backed view. Modern Mendix projects keep the
1092
+ # query in a separate ViewEntitySourceDocument; older projects may keep
1093
+ # it directly on the embedded entity.
1094
+ def oql_view(source: nil, query: nil)
1095
+ raise ArgumentError, 'oql_view requires source or query' if source.nil? && query.nil?
1096
+
1097
+ @oql_view = { source: source&.to_s, query: query&.to_s }.compact
1098
+ @persistable = false
1099
+ end
1100
+
1084
1101
  def generalizes(entity)
1085
1102
  @generalization = entity.to_s
1086
1103
  end
@@ -1164,7 +1181,7 @@ module Mxrb
1164
1181
  name: @name, persistable: @persistable, documentation: @doc,
1165
1182
  attributes: @attributes, associations: @associations, lifecycle: @lifecycle,
1166
1183
  access_rules: @access_rules, generalization: @generalization,
1167
- system_members: @system_members, indexes: @indexes
1184
+ system_members: @system_members, indexes: @indexes, oql_view: @oql_view
1168
1185
  }
1169
1186
  end
1170
1187
 
data/lib/mxrb/exporter.rb CHANGED
@@ -9,6 +9,7 @@ module Mxrb
9
9
  # Exports an MPR into an editable, layered Ruby source tree.
10
10
  class Exporter
11
11
  LAYERS = %w[domain application presentation infrastructure].freeze
12
+ MODULE_PROGRESS_WEIGHT = 10
12
13
  MARKETPLACE_PROVENANCE = [
13
14
  ".mxrb/marketplace.lock.json",
14
15
  ".mxrb/marketplace",
@@ -63,22 +64,30 @@ module Mxrb
63
64
  end
64
65
 
65
66
  def export!(parallel: true)
66
- FileUtils.mkdir_p(@output_dir)
67
- Mxrb.open(@mpr_path) do |project|
68
- @architecture = project.architecture_definition
69
- export_app_structure
70
- export_project_assets
71
- export_native_units(project)
72
- export_security(project)
73
- export_architecture_contracts(project)
74
- mods = project.modules
75
- if parallel && mods.size > 1
76
- threads = mods.map { |mod| Thread.new { export_module(mod) } }
77
- threads.each(&:join)
78
- else
79
- mods.each { export_module(_1) }
67
+ Progress.with("Exporting #{File.basename(@mpr_path)}") do |progress|
68
+ FileUtils.mkdir_p(@output_dir)
69
+ Mxrb.open(@mpr_path) do |project|
70
+ @architecture = project.architecture_definition
71
+ units = project.all_units
72
+ modules = project.modules
73
+ assets = project_asset_files
74
+ progress.update(
75
+ current: 0,
76
+ total: 4 + assets.size + (units.size * 2) + (modules.size * MODULE_PROGRESS_WEIGHT),
77
+ detail: "preparing Ruby project"
78
+ )
79
+ export_app_structure
80
+ progress.advance(detail: "project structure")
81
+ export_project_assets(assets, progress)
82
+ export_native_units(project, units, progress)
83
+ export_security(project)
84
+ progress.advance(detail: "project security")
85
+ export_architecture_contracts(project)
86
+ progress.advance(detail: "navigation and design system")
87
+ export_modules(modules, progress, parallel:)
88
+ write_project(project)
89
+ progress.advance(detail: "project.rb")
80
90
  end
81
- write_project(project)
82
91
  end
83
92
  @output_dir
84
93
  end
@@ -91,6 +100,8 @@ module Mxrb
91
100
 
92
101
  export_domain(root, mod)
93
102
  export_microflows(root, mod)
103
+ export_application_documents(root, mod)
104
+ export_infrastructure_documents(root, mod)
94
105
  export_pages(root, mod)
95
106
  export_menus(root, mod)
96
107
  export_nanoflows(root, mod)
@@ -106,25 +117,31 @@ module Mxrb
106
117
  ].each { write(File.join(@output_dir, _1, ".keep"), "") }
107
118
  end
108
119
 
109
- def export_project_assets
120
+ def project_asset_files
110
121
  source_root = File.dirname(@mpr_path)
111
- files = (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory|
122
+ (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory|
112
123
  root = File.join(source_root, directory)
113
124
  next [root] if File.file?(root) && !File.symlink?(root)
114
125
  next [] unless File.directory?(root) && !File.symlink?(root)
115
126
 
116
127
  Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH)
117
128
  end.select { File.file?(_1) && !File.symlink?(_1) }.sort
129
+ end
130
+
131
+ def export_project_assets(files, progress)
132
+ source_root = File.dirname(@mpr_path)
118
133
  entries = files.map do |source|
119
134
  relative = Pathname.new(source).relative_path_from(Pathname.new(source_root)).to_s
120
135
  target = File.join(@output_dir, relative)
121
136
  FileUtils.mkdir_p(File.dirname(target))
122
137
  FileUtils.cp(source, target)
123
- {
138
+ result = {
124
139
  "path" => relative,
125
140
  "size" => File.size(source),
126
141
  "sha256" => Digest::SHA256.file(source).hexdigest
127
142
  }
143
+ progress.advance(detail: "asset #{relative}")
144
+ result
128
145
  end
129
146
  write(
130
147
  File.join(@output_dir, ".mxrb", "assets.json"),
@@ -132,8 +149,8 @@ module Mxrb
132
149
  )
133
150
  end
134
151
 
135
- def export_native_units(project)
136
- units = project.all_units.filter_map do |unit|
152
+ def export_native_units(project, project_units, progress)
153
+ units = project_units.filter_map do |unit|
137
154
  doc = project.parse_bson(unit)
138
155
  type = doc["$Type"]
139
156
  next if type.to_s.empty? || type == "Projects$Project"
@@ -147,16 +164,21 @@ module Mxrb
147
164
  "type" => type,
148
165
  "contents" => Base64.strict_encode64(IO::BsonCodec.serialize(doc))
149
166
  }
167
+ ensure
168
+ progress.advance(detail: "native baseline")
150
169
  end
151
170
  write(
152
171
  File.join(@output_dir, ".mxrb", "native_units.json"),
153
172
  JSON.pretty_generate("format_version" => project.format_version.to_s, "units" => units)
154
173
  )
155
- source = project.all_units.filter_map do |unit|
174
+ source = project_units.filter_map do |unit|
156
175
  doc = project.parse_bson(unit)
157
176
  next if doc["$Type"].to_s.empty? || doc["$Type"] == "Projects$Project"
177
+ next if Model::Module::EDITABLE_DOCUMENT_TYPES.include?(doc["$Type"])
158
178
 
159
179
  native_unit_source(project, unit, doc)
180
+ ensure
181
+ progress.advance(detail: "editable native units")
160
182
  end
161
183
  write(
162
184
  File.join(@output_dir, ".mxrb", "native_units.rb"),
@@ -164,6 +186,18 @@ module Mxrb
164
186
  )
165
187
  end
166
188
 
189
+ def export_modules(modules, progress, parallel:)
190
+ operation = lambda do |mod|
191
+ export_module(mod)
192
+ progress.advance(MODULE_PROGRESS_WEIGHT, detail: "module #{mod.name}")
193
+ end
194
+ if parallel && modules.size > 1
195
+ modules.map { |mod| Thread.new { operation.call(mod) } }.each(&:join)
196
+ else
197
+ modules.each { operation.call(_1) }
198
+ end
199
+ end
200
+
167
201
  def native_unit_source(project, unit, doc)
168
202
  <<~RUBY
169
203
  native_unit #{ruby(unit.fetch("UnitID"))},
@@ -211,8 +245,10 @@ module Mxrb
211
245
 
212
246
  def export_module_scaffolding(root)
213
247
  %w[
214
- domain/enumerations domain/rules domain/policies
215
- application/ports/repositories application/queries application/validations application/jobs
248
+ domain/entities domain/dtos domain/oql_views domain/enumerations domain/constants
249
+ domain/rules domain/policies
250
+ application/ports/repositories application/queries application/queries/datasets
251
+ application/validations application/jobs application/jobs/scheduled_events
216
252
  presentation/features presentation/client_actions presentation/snippets presentation/view_models
217
253
  presentation/menus
218
254
  infrastructure/persistence/mendix infrastructure/persistence/external
@@ -238,26 +274,62 @@ module Mxrb
238
274
 
239
275
  def export_domain(root, mod)
240
276
  domain = File.join(root, "domain")
241
- entities_dir = File.join(domain, "entities")
242
- FileUtils.mkdir_p(entities_dir)
243
-
244
277
  associations = mod.associations.group_by(&:from_entity_id)
245
278
  architecture = architecture_module(mod.name)
246
- entity_files = unique_entity_filenames(mod.entities)
279
+ paths = {}
280
+ grouped = mod.entities.group_by { entity_domain_route(_1) }
281
+ grouped.each do |route, entities|
282
+ unique_entity_filenames(entities).each do |id, filename|
283
+ paths[id] = File.join(route, filename)
284
+ end
285
+ end
286
+ domain_documents = mod.domain_documents
287
+ oql_documents = mod.oql_view_documents
288
+ attached_oql_documents = {}
247
289
  mod.entities.each do |entity|
248
290
  entity_metadata = architecture&.fetch(:entities, [])&.find { _1[:name] == entity.name }
291
+ source = entity_source(entity, mod, associations.fetch(entity.id, []), entity_metadata)
292
+ if oql_view_entity?(entity) && (document = oql_document_for(entity, oql_documents, mod.name))
293
+ source = "#{source.rstrip}\n\n#{native_document_declaration(document)}"
294
+ attached_oql_documents[document.fetch(:id)] = true
295
+ end
249
296
  write(
250
- File.join(entities_dir, entity_files.fetch(entity.id)),
251
- entity_source(entity, mod, associations.fetch(entity.id, []), entity_metadata)
297
+ File.join(domain, paths.fetch(entity.id)), source
252
298
  )
253
299
  end
254
-
255
- loads = entity_files.values.sort.map do |filename|
256
- %(evaluate File.join(__dir__, "entities", #{ruby(filename)}))
300
+ used = paths.values.to_h { [_1, true] }
301
+ domain_documents.reject { attached_oql_documents[_1.fetch(:id)] }.each do |document|
302
+ relative = unique_relative_path(document.fetch(:route), underscore(document.fetch(:name)), used)
303
+ write(File.join(domain, relative), mapping_document_source(document))
304
+ paths[document.fetch(:id)] = relative
305
+ end
306
+ loads = paths.values.sort.map do |relative|
307
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
308
+ %(evaluate File.join(__dir__, #{segments}))
257
309
  end
258
310
  write(File.join(domain, "model.rb"), "#{loads.join("\n")}\n")
259
311
  end
260
312
 
313
+ def entity_domain_route(entity)
314
+ return 'oql_views' if oql_view_entity?(entity)
315
+ return 'dtos' unless entity.persistable
316
+
317
+ 'entities'
318
+ end
319
+
320
+ def oql_view_entity?(entity)
321
+ entity.respond_to?(:oql_view?) && entity.oql_view?
322
+ end
323
+
324
+ def oql_document_for(entity, documents, module_name)
325
+ reference = entity.oql_source_document.to_s
326
+ return nil if reference.empty?
327
+
328
+ documents.find do |document|
329
+ [document.fetch(:name), "#{module_name}.#{document.fetch(:name)}"].include?(reference)
330
+ end
331
+ end
332
+
261
333
  def export_microflows(root, mod)
262
334
  files = unique_filenames(mod.microflows)
263
335
  by_layer = { "application" => [], "infrastructure" => [] }
@@ -296,6 +368,74 @@ module Mxrb
296
368
  write_path_aggregator(File.join(presentation, "presentation.rb"), paths)
297
369
  end
298
370
 
371
+ def export_infrastructure_documents(root, mod)
372
+ documents = mod.infrastructure_documents
373
+ return if documents.empty?
374
+
375
+ infrastructure = File.join(root, "infrastructure")
376
+ used = {}
377
+ paths = documents.map do |document|
378
+ base = underscore(document.fetch(:name))
379
+ relative = unique_relative_path(document.fetch(:route), base, used)
380
+ write(File.join(infrastructure, relative), mapping_document_source(document))
381
+ relative
382
+ end
383
+ append_to_aggregator(File.join(infrastructure, "infrastructure.rb"), paths)
384
+ end
385
+
386
+ def export_application_documents(root, mod)
387
+ documents = mod.application_documents
388
+ return if documents.empty?
389
+
390
+ application = File.join(root, 'application')
391
+ used = {}
392
+ paths = documents.map do |document|
393
+ relative = unique_relative_path(document.fetch(:route), underscore(document.fetch(:name)), used)
394
+ write(File.join(application, relative), mapping_document_source(document))
395
+ relative
396
+ end
397
+ append_to_aggregator(File.join(application, 'application.rb'), paths)
398
+ end
399
+
400
+ def unique_relative_path(directory, base, used)
401
+ candidate = File.join(directory, "#{base}.rb")
402
+ suffix = 2
403
+ while used[candidate]
404
+ candidate = File.join(directory, "#{base}_#{suffix}.rb")
405
+ suffix += 1
406
+ end
407
+ used[candidate] = true
408
+ candidate
409
+ end
410
+
411
+ def mapping_document_source(document)
412
+ <<~RUBY
413
+ # frozen_string_literal: true
414
+
415
+ #{native_document_declaration(document)}
416
+ RUBY
417
+ end
418
+
419
+ def native_document_declaration(document)
420
+ <<~RUBY.rstrip
421
+ native_document #{symbol(document.fetch(:name))},
422
+ type: #{ruby(document.fetch(:type))},
423
+ unit_id: #{ruby(document.fetch(:id))},
424
+ container_id: #{ruby(document.fetch(:container_id))},
425
+ containment: #{ruby(document.fetch(:containment))},
426
+ deep_structure: #{native_ruby(document.fetch(:doc), 24)}
427
+ RUBY
428
+ end
429
+
430
+ def append_to_aggregator(path, relative_paths)
431
+ existing = File.exist?(path) ? File.read(path).lines : []
432
+ additions = relative_paths.sort.map do |relative|
433
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
434
+ "evaluate File.join(__dir__, #{segments})\n"
435
+ end
436
+ write(path, (existing + additions).uniq.join)
437
+ end
438
+
299
439
  def export_menus(root, mod)
300
440
  menus = mod.menus
301
441
  return if menus.empty?
@@ -586,6 +726,12 @@ module Mxrb
586
726
  end
587
727
  flags = []
588
728
  flags << " non_persistent!" unless entity.persistable
729
+ if oql_view_entity?(entity)
730
+ options = []
731
+ options << "source: #{ruby(entity.oql_source_document)}" if entity.oql_source_document
732
+ options << "query: #{ruby(entity.oql_query)}" unless entity.oql_query.to_s.empty?
733
+ flags << " oql_view #{options.join(', ')}" unless options.empty?
734
+ end
589
735
  flags << " documentation #{ruby(entity.documentation)}" unless entity.documentation.to_s.empty?
590
736
  generalization = entity.respond_to?(:generalization_target) ? entity.generalization_target : nil
591
737
  flags << " generalizes #{ruby(generalization)}" if generalization
@@ -11,19 +11,26 @@ module Mxrb
11
11
  class Validator
12
12
  def initialize(path)
13
13
  @path = path
14
+ @progress = Progress::NullTask.instance
14
15
  end
15
16
 
16
17
  def validate
17
- @errors = []
18
- @warnings = []
19
- @mpr = IO::MprFile.open(@path, readonly: true)
20
- validate_tables
21
- validate_units
22
- validate_v2_files
23
- Result.new(errors: @errors, warnings: @warnings)
18
+ Progress.with("Validating #{File.basename(@path)}") do |progress|
19
+ @progress = progress
20
+ @errors = []
21
+ @warnings = []
22
+ @mpr = IO::MprFile.open(@path, readonly: true)
23
+ validate_tables
24
+ progress.advance(detail: "MPR tables")
25
+ validate_units
26
+ validate_v2_files
27
+ progress.advance(detail: "v2 contents")
28
+ Result.new(errors: @errors, warnings: @warnings)
29
+ end
24
30
  rescue Error => e
25
31
  Result.new(errors: [e.message], warnings: @warnings || [])
26
32
  ensure
33
+ @progress = Progress::NullTask.instance
27
34
  @mpr&.close
28
35
  end
29
36
 
@@ -40,8 +47,10 @@ module Mxrb
40
47
 
41
48
  def validate_units
42
49
  @units = @mpr.all_units
50
+ @progress.update(total: @units.size + 3, detail: "#{@units.size} units")
43
51
  validate_root
44
52
  validate_unit_ids
53
+ @progress.advance(detail: "unit tree")
45
54
  validate_contents
46
55
  end
47
56
 
@@ -78,6 +87,8 @@ module Mxrb
78
87
  next unless doc
79
88
 
80
89
  validate_doc_identity(unit, doc)
90
+ ensure
91
+ @progress&.advance(detail: "unit #{unit['UnitID']}")
81
92
  end
82
93
  end
83
94
 
@@ -10,7 +10,7 @@ module Mxrb
10
10
  attr_accessor :id, :name, :qualified_name, :documentation,
11
11
  :persistable, :location, :data_storage_guid,
12
12
  :export_level, :generalization, :access_rules, :indexes,
13
- :system_members
13
+ :system_members, :source, :oql_query, :native_type
14
14
 
15
15
  # Build from a BSON hash (embedded in DomainModel's "entities" array).
16
16
  def self.from_bson(doc, _domain_model_id, mpr)
@@ -19,6 +19,9 @@ module Mxrb
19
19
  e.name = doc["name"] || doc["Name"]
20
20
  e.qualified_name = doc["$QualifiedName"] || doc["\$QualifiedName"]
21
21
  e.documentation = doc["documentation"] || doc["Documentation"] || ""
22
+ e.native_type = doc["$Type"]
23
+ e.source = doc["source"] || doc["Source"]
24
+ e.oql_query = doc["oqlQuery"] || doc["OqlQuery"] || doc["OQLQuery"]
22
25
  e.data_storage_guid = doc["dataStorageGuid"] || doc["DataStorageGuid"]
23
26
  e.export_level = doc["exportLevel"] || doc["ExportLevel"] || "Hidden"
24
27
  e.location = parse_location(doc["location"] || doc["Location"])
@@ -58,6 +61,19 @@ module Mxrb
58
61
  @generalization['generalization'] || @generalization['Generalization']
59
62
  end
60
63
 
64
+ def oql_view?
65
+ source_type = @source.is_a?(Hash) ? @source.fetch('$Type', '') : ''
66
+ @native_type.to_s.match?(/ViewEntity/i) ||
67
+ source_type.match?(/OqlViewEntitySource/i) ||
68
+ !@oql_query.to_s.empty?
69
+ end
70
+
71
+ def oql_source_document
72
+ return unless @source.is_a?(Hash)
73
+
74
+ @source['SourceDocument'] || @source['sourceDocument']
75
+ end
76
+
61
77
  def to_bson
62
78
  {
63
79
  "$ID" => @id,