mxrb 0.1.0

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 (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,483 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+
6
+ module Mxrb
7
+ module Compiler
8
+ # Lowers supported client-flow graphs to the instruction programs consumed by the Mendix client.
9
+ # rubocop:disable Metrics
10
+ class NanoflowProgramCompiler
11
+ include ModelValues
12
+
13
+ attr_reader :unsupported
14
+
15
+ def initialize(source)
16
+ @source = source
17
+ @nanoflows = source.units_of('Microflows$Nanoflow').to_h do |unit|
18
+ ["#{unit.module_name}.#{unit.document['Name']}", unit]
19
+ end
20
+ @programs = {}
21
+ @unsupported = []
22
+ end
23
+
24
+ def reference(qualified_name)
25
+ if @programs.key?(qualified_name)
26
+ identifier = @programs[qualified_name]&.fetch(:reference)
27
+ return identifier && "() => #{identifier}"
28
+ end
29
+
30
+ unit = nanoflow_unit(qualified_name)
31
+ return unless unit
32
+
33
+ compile(unit, qualified_name)
34
+ end
35
+
36
+ def declarations
37
+ @programs.values.filter_map { _1[:declaration] }.join("\n")
38
+ end
39
+
40
+ private
41
+
42
+ def compile(unit, qualified_name)
43
+ reference = "mxrbNanoflow_#{Digest::SHA256.hexdigest(qualified_name)[0, 12]}"
44
+ @programs[qualified_name] = { reference:, declaration: nil }
45
+ (@variable_kind_stack ||= []) << variable_kinds(unit.document)
46
+ (@flow_stack ||= []) << qualified_name
47
+ unsupported_count = @unsupported.length
48
+ instructions = compile_graph(unit.document)
49
+ return @programs.delete(qualified_name) && nil if @unsupported.length > unsupported_count
50
+
51
+ program = { name: qualified_name, useListParameterByReference: true, instructions: }
52
+ @programs[qualified_name][:declaration] = "const #{reference} = #{js_literal(program)};"
53
+ "() => #{reference}"
54
+ ensure
55
+ @flow_stack.pop
56
+ @variable_kind_stack.pop
57
+ end
58
+
59
+ def variable_kinds(document)
60
+ array(document.dig('ObjectCollection', 'Objects')).each_with_object({}) do |object, result|
61
+ case object['$Type']
62
+ when 'Microflows$MicroflowParameter'
63
+ result[object['Name']] = variable_kind(object['VariableType'])
64
+ when 'Microflows$ActionActivity'
65
+ action = object['Action'] || {}
66
+ result[action['VariableName']] = variable_kind(action['VariableType']) if action['VariableName']
67
+ if %w[Microflows$CreateObjectAction
68
+ Microflows$CreateChangeAction].include?(action['$Type']) && action['VariableName']
69
+ result[action['VariableName']] = 'object'
70
+ end
71
+ result[action['OutputVariableName']] = 'object' if action['OutputVariableName']
72
+ result[action['ResultVariableName']] = 'primitive' if action['ResultVariableName']
73
+ end
74
+ end
75
+ end
76
+
77
+ def graph(document)
78
+ objects = array(document.dig('ObjectCollection', 'Objects'))
79
+ by_id = objects.to_h { [model_id(_1), _1] }
80
+ flows = array(document['Flows']).group_by { model_id(_1['OriginPointer']) }
81
+ [objects, by_id, flows]
82
+ end
83
+
84
+ def compile_graph(document)
85
+ objects, by_id, flows = graph(document)
86
+ start = objects.find { _1['$Type'] == 'Microflows$StartEvent' }
87
+ ordered = reachable_nodes(start, by_id, flows)
88
+ ordered.flat_map { compile_graph_node(_1, flows.fetch(model_id(_1), [])) }.compact
89
+ end
90
+
91
+ def reachable_nodes(start, by_id, flows)
92
+ queue = (start ? [start] : []).flat_map { flows.fetch(model_id(_1), []) }
93
+ .map { by_id[model_id(_1['DestinationPointer'])] }
94
+ visited = {}
95
+ [].tap do |result|
96
+ until queue.empty?
97
+ current = queue.shift
98
+ next unless current
99
+ next if visited[model_id(current)]
100
+
101
+ visited[model_id(current)] = true
102
+ result << current
103
+ queue.concat(flows.fetch(model_id(current), []).map do |flow|
104
+ by_id[model_id(flow['DestinationPointer'])]
105
+ end)
106
+ end
107
+ end
108
+ end
109
+
110
+ def compile_graph_node(node, flows)
111
+ error_flows = flows.select { _1['IsErrorHandler'] == true }
112
+ normal_flows = flows.reject { _1['IsErrorHandler'] == true }
113
+ instruction = if error_flows.empty?
114
+ compile_node(node, normal_flows)
115
+ else
116
+ compile_try_catch(node, normal_flows, error_flows)
117
+ end
118
+ instructions = instruction.is_a?(Array) ? instruction : [instruction].compact
119
+ return instructions if terminal_node?(node) || instruction.nil?
120
+ return instructions unless normal_flows.one?
121
+
122
+ instructions << { type: 'jump', label: "#{model_id(node)}$next",
123
+ target: model_id(normal_flows.first['DestinationPointer']) }
124
+ end
125
+
126
+ def compile_try_catch(node, normal_flows, error_flows)
127
+ return unsupported!(node, 'Microflows$ErrorHandler') unless normal_flows.one? && error_flows.one?
128
+
129
+ compiled = compile_node(node, normal_flows)
130
+ body = (compiled.is_a?(Array) ? compiled : [compiled]).compact.map { _1.except(:label) }
131
+ return if body.empty?
132
+
133
+ body << { type: 'return', result: { type: 'literal', value: true }, resultKind: 'primitive' }
134
+ {
135
+ type: 'tryCatch', label: model_id(node),
136
+ catchTarget: model_id(error_flows.first['DestinationPointer']), body:
137
+ }
138
+ end
139
+
140
+ def terminal_node?(node)
141
+ %w[Microflows$EndEvent Microflows$ExclusiveSplit Microflows$ExclusiveMerge]
142
+ .include?(node['$Type'])
143
+ end
144
+
145
+ def compile_node(node, flows = [])
146
+ return compile_end(node) if node['$Type'] == 'Microflows$EndEvent'
147
+ return compile_split(node, flows) if node['$Type'] == 'Microflows$ExclusiveSplit'
148
+ return compile_merge(node, flows) if node['$Type'] == 'Microflows$ExclusiveMerge'
149
+ return unsupported!(node, node['$Type']) unless node['$Type'] == 'Microflows$ActionActivity'
150
+
151
+ action = node['Action'] || {}
152
+ case action['$Type']
153
+ when 'Microflows$CreateVariableAction' then compile_variable(action, node)
154
+ when 'Microflows$ChangeVariableAction' then compile_change_variable(action, node)
155
+ when 'Microflows$CreateObjectAction' then compile_create_object(action, node)
156
+ when 'Microflows$CreateChangeAction' then compile_create_object(action, node)
157
+ when 'Microflows$ChangeAction' then compile_change_object(action, node)
158
+ when 'Microflows$NanoflowCallAction' then compile_nanoflow_call(action, node)
159
+ when 'Microflows$MicroflowCallAction' then compile_microflow_call(action, node)
160
+ when 'Microflows$JavaScriptActionCallAction' then compile_javascript_call(action, node)
161
+ when 'Microflows$ShowFormAction' then compile_show_form(action, node)
162
+ when 'Microflows$CloseFormAction' then compile_close_form(action, node)
163
+ when 'Microflows$ShowMessageAction' then compile_show_message(action, node)
164
+ when 'Microflows$ValidationFeedbackAction' then compile_validation(action, node)
165
+ when 'Microflows$LogMessageAction' then compile_log(action, node)
166
+ when 'Microflows$CommitAction' then compile_commit(action, node)
167
+ else unsupported!(node, action['$Type'])
168
+ end
169
+ end
170
+
171
+ def compile_end(node)
172
+ { type: 'return', label: model_id(node), result: expression(node['ReturnValue']),
173
+ resultKind: expression_kind(node['ReturnValue']) }
174
+ end
175
+
176
+ def compile_variable(action, node)
177
+ { type: 'setVariable', label: model_id(node), outputVar: action['VariableName'],
178
+ outputKind: variable_kind(action['VariableType']), value: expression(action['InitialValue']) }
179
+ end
180
+
181
+ def compile_change_variable(action, node)
182
+ { type: 'setVariable', label: model_id(node), outputVar: action['ChangeVariableName'],
183
+ outputKind: 'primitive', value: expression(action['Value']) }
184
+ end
185
+
186
+ def compile_create_object(action, node)
187
+ variable = action['OutputVariableName'] || action['VariableName']
188
+ create = { type: 'createObject', label: model_id(node), objectType: action['Entity'],
189
+ outputVar: variable }
190
+ instructions = [create] + change_instructions(action, variable, "#{model_id(node)}$change")
191
+ instructions << compile_commit(action, node, label: false) if action['Commit'] == 'Yes'
192
+ instructions.compact
193
+ end
194
+
195
+ def compile_change_object(action, node)
196
+ variable = action['ChangeVariableName']
197
+ changes = change_instructions(action, variable, model_id(node))
198
+ changes << compile_commit(action, node, label: false) if action['Commit'] == 'Yes'
199
+ changes.compact
200
+ end
201
+
202
+ def change_instructions(action, variable, label)
203
+ array(action['Items']).map.with_index do |item, index|
204
+ member = item['Attribute'].to_s.split('.').last.to_s
205
+ next unsupported!(item, item['$Type']) if member.empty? || item['Type'] != 'Set'
206
+
207
+ { type: 'changeObject', label: index.zero? ? label : "#{label}$#{index}",
208
+ inputVar: variable, member:, value: expression(item['Value']) }
209
+ end.compact
210
+ end
211
+
212
+ def compile_nanoflow_call(action, node = action)
213
+ call = action['NanoflowCall'] || {}
214
+ target = call['Nanoflow'].to_s
215
+ reference = reference(target)
216
+ return unsupported!(action, action['$Type']) unless reference
217
+
218
+ result = { type: 'nanoflowCall', label: model_id(node), flow: raw(reference),
219
+ parameters: call_parameters(call) }
220
+ output = action['OutputVariableName'].to_s
221
+ result[:outputVar] = output unless output.empty?
222
+ result
223
+ end
224
+
225
+ def compile_microflow_call(action, node)
226
+ call = action['MicroflowCall'] || {}
227
+ name = call['Microflow'].to_s
228
+ return unsupported!(action, action['$Type']) if name.empty?
229
+
230
+ result = {
231
+ type: 'microflowCall', label: model_id(node),
232
+ operationId: WebOperationCompiler.operation_id(current_flow, model_id(node)),
233
+ parameters: call_parameters(call)
234
+ }
235
+ output = action['ResultVariableName'].to_s
236
+ result[:outputVar] = output if action['UseReturnVariable'] == true && !output.empty?
237
+ result
238
+ end
239
+
240
+ def compile_javascript_call(action, node)
241
+ qualified = action['JavaScriptAction'].to_s
242
+ unit = @source.units_of('JavaScriptActions$JavaScriptAction').find do |candidate|
243
+ "#{candidate.module_name}.#{candidate.document['Name']}" == qualified
244
+ end
245
+ return unsupported!(action, action['$Type']) unless unit
246
+
247
+ result = {
248
+ type: 'javaScriptActionCall', label: model_id(node), action: raw(javascript_reference(unit)),
249
+ parameters: javascript_parameters(action, unit)
250
+ }
251
+ output = action['OutputVariableName'].to_s
252
+ result[:outputVar] = output if action['UseReturnVariable'] == true && !output.empty?
253
+ result
254
+ end
255
+
256
+ def javascript_reference(unit)
257
+ module_path = unit.module_name.downcase
258
+ name = unit.document['Name']
259
+ path = File.join(File.dirname(@source.path), 'javascriptsource', module_path, 'actions', name)
260
+ "() => require(#{JSON.generate(path)}).#{name}"
261
+ end
262
+
263
+ def javascript_parameters(action, unit)
264
+ parameter_types = array(unit.document['Parameters']).to_h do |parameter|
265
+ [parameter['Name'], javascript_parameter_kind(parameter)]
266
+ end
267
+ array(action['ParameterMappings']).map do |mapping|
268
+ name = mapping['Parameter'].to_s.split('.').last
269
+ value = mapping['ParameterValue'] || {}
270
+ argument = value['Argument'] || quoted_entity(value['Entity'])
271
+ { kind: parameter_types.fetch(name, 'primitive'), value: expression(argument) }
272
+ end
273
+ end
274
+
275
+ def quoted_entity(entity)
276
+ entity.to_s.empty? ? nil : JSON.generate(entity.to_s)
277
+ end
278
+
279
+ def javascript_parameter_kind(parameter)
280
+ type = parameter.dig('ParameterType', 'Type', '$Type').to_s
281
+ return 'object' if type.include?('EntityType')
282
+ return 'list' if type.include?('ListType')
283
+
284
+ 'primitive'
285
+ end
286
+
287
+ def compile_split(node, flows)
288
+ targets = flows.to_h do |flow|
289
+ case_value = array(flow['CaseValues']).first
290
+ key = case_value&.fetch('Value', nil).to_s
291
+ key = '' if case_value&.fetch('$Type', nil) == 'Microflows$NoCase'
292
+ [key, model_id(flow['DestinationPointer'])]
293
+ end
294
+ condition = node.dig('SplitCondition', 'Expression')
295
+ return unsupported!(node, node['$Type']) if targets.empty? || condition.to_s.empty?
296
+
297
+ { type: 'switch', label: model_id(node), condition: expression(condition), targets: }
298
+ end
299
+
300
+ def compile_merge(node, flows)
301
+ target = flows.one? && model_id(flows.first['DestinationPointer'])
302
+ return unsupported!(node, node['$Type']) unless target
303
+
304
+ { type: 'jump', label: model_id(node), target: }
305
+ end
306
+
307
+ def compile_show_form(action, node)
308
+ settings = action['FormSettings'] || {}
309
+ form = settings['Form'].to_s
310
+ return unsupported!(action, action['$Type']) if form.empty?
311
+
312
+ path = "#{form.tr('.', '/')}.page.xml"
313
+ params = { name: path, location: 'modal', resizable: false }
314
+ mappings = array(settings['ParameterMappings']).to_h do |mapping|
315
+ ["$#{mapping['Parameter'].to_s.split('.').last}", expression(mapping['Argument'])]
316
+ end
317
+ { type: 'openForm', label: model_id(node), path:, params: }.tap do |instruction|
318
+ instruction[:inputArgs] = mappings unless mappings.empty?
319
+ end
320
+ end
321
+
322
+ def compile_close_form(action, node)
323
+ value = action['NumberOfPagesToClose'].to_s
324
+ { type: 'closeForm', label: model_id(node) }.tap do |instruction|
325
+ instruction[:numberOfPagesToClose] = expression(value) unless value.empty?
326
+ end
327
+ end
328
+
329
+ def compile_show_message(action, node)
330
+ { type: 'showMessage', label: model_id(node),
331
+ message: text_template_expression(action['Template']),
332
+ messageType: action['Type'].to_s.downcase, blocking: action['Blocking'] == true }
333
+ end
334
+
335
+ def compile_validation(action, node)
336
+ member = action['Attribute'].to_s.split('.').last.to_s
337
+ return unsupported!(action, action['$Type']) if member.empty?
338
+
339
+ { type: 'showValidation', label: model_id(node), inputVar: action['ValidationVariableName'],
340
+ member:, text: text_template_expression(action['FeedbackTemplate']) }
341
+ end
342
+
343
+ def compile_log(action, node)
344
+ { type: 'writeLog', label: model_id(node), level: action['Level'].to_s.downcase,
345
+ message: text_template_expression(action['MessageTemplate']) }
346
+ end
347
+
348
+ def compile_commit(action, node, label: true)
349
+ input = (action['CommitVariableName'] || action['ChangeVariableName'] ||
350
+ action['OutputVariableName'] || action['VariableName']).to_s
351
+ return unsupported!(action, action['$Type']) if input.empty?
352
+
353
+ { type: 'commitObjects', inputVar: input,
354
+ operationId: WebOperationCompiler.operation_id(current_flow, model_id(node)) }.tap do |result|
355
+ result[:label] = model_id(node) if label
356
+ end
357
+ end
358
+
359
+ def text_template_expression(template)
360
+ text = template&.fetch('Text', nil)
361
+ value = if text.is_a?(Hash)
362
+ items = array(text['Items'])
363
+ items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', nil) ||
364
+ items.first&.fetch('Text', nil).to_s
365
+ else
366
+ text.to_s
367
+ end
368
+ parameters = array(template&.fetch('Parameters', nil))
369
+ return expression(value) if parameters.empty?
370
+
371
+ arguments = parameters.map { expression(_1['Expression'] || _1['Argument']) }
372
+ { type: 'function', name: 'formatString', parameters: [{ type: 'literal', value: }] + arguments }
373
+ end
374
+
375
+ def variable_kind(type)
376
+ type&.fetch('$Type', nil).to_s.include?('Object') ? 'object' : 'primitive'
377
+ end
378
+
379
+ def call_parameters(call)
380
+ array(call['ParameterMappings']).filter_map do |mapping|
381
+ next unless mapping.is_a?(Hash)
382
+
383
+ { name: mapping['Parameter'].to_s.split('.').last,
384
+ value: expression(mapping['Argument']), kind: expression_kind(mapping['Argument']) }
385
+ end
386
+ end
387
+
388
+ def expression(value)
389
+ source = value.to_s.strip
390
+ return { type: 'literal', value: nil } if source.empty? || source == 'empty'
391
+
392
+ conditional = conditional_expression(source)
393
+ return conditional if conditional
394
+
395
+ function = function_expression(source)
396
+ return function if function
397
+
398
+ binary = binary_expression(source)
399
+ return binary if binary
400
+ return variable_expression(source) if source.match?(%r{\A\$[A-Za-z_]\w*(?:/[A-Za-z_]\w*)*\z})
401
+ return { type: 'constant', name: source.delete_prefix('@') } if source.start_with?('@')
402
+ return { type: 'literal', value: source == 'true' } if %w[true false].include?(source)
403
+ return { type: 'literal', value: source[1..-2] } if quoted?(source)
404
+ return { type: 'literalNumeric', value: source } if source.match?(/\A-?\d+(?:\.\d+)?\z/)
405
+
406
+ { type: 'literal', value: source }
407
+ end
408
+
409
+ def expression_kind(value)
410
+ name = value.to_s.strip[/\A\$([A-Za-z_]\w*)/, 1]
411
+ name ? current_variable_kinds.fetch(name, 'primitive') : 'primitive'
412
+ end
413
+
414
+ def current_variable_kinds = @variable_kind_stack&.last || {}
415
+
416
+ def variable_expression(source)
417
+ variable, *path = source.delete_prefix('$').split('/')
418
+ { type: 'variable', variable: }.tap do |result|
419
+ result[:path] = path.join('/') unless path.empty?
420
+ end
421
+ end
422
+
423
+ def conditional_expression(source)
424
+ match = source.match(/\Aif\s+(.+?)\s+then\s+(.+?)\s+else\s+(.+)\z/im)
425
+ return unless match
426
+
427
+ { type: 'conditional', condition: expression(match[1]),
428
+ then: expression(match[2]), else: expression(match[3]) }
429
+ end
430
+
431
+ def function_expression(source)
432
+ match = source.match(/\A(not|isNew|isSynced)\((.*)\)\z/im)
433
+ return unless match
434
+
435
+ { type: 'function', name: match[1], parameters: [expression(match[2])] }
436
+ end
437
+
438
+ def binary_expression(source)
439
+ match = source.match(/\A(.+?)\s+(and|or|!=|=|>=|<=|>|<)\s+(.+)\z/im)
440
+ return unless match
441
+
442
+ { type: 'function', name: match[2].downcase,
443
+ parameters: [expression(match[1]), expression(match[3])] }
444
+ end
445
+
446
+ def quoted?(value)
447
+ (value.start_with?("'") && value.end_with?("'")) ||
448
+ (value.start_with?('"') && value.end_with?('"'))
449
+ end
450
+
451
+ def unsupported!(node, type)
452
+ unit = current_flow
453
+ label = type.to_s.empty? ? node['$Type'] : type
454
+ @unsupported << "#{unit}:#{label}"
455
+ nil
456
+ end
457
+
458
+ def current_flow = @flow_stack&.last || @programs.keys.last
459
+
460
+ def nanoflow_unit(qualified_name)
461
+ @nanoflows[qualified_name]
462
+ end
463
+
464
+ def model_id(value)
465
+ identifier = value.is_a?(Hash) && value.key?('$ID') ? value['$ID'] : value
466
+ IO::BsonCodec.extract_id(identifier).to_s
467
+ end
468
+
469
+ def raw(value) = { '$raw' => value }
470
+
471
+ def js_literal(value)
472
+ return value['$raw'] if value.is_a?(Hash) && value.key?('$raw')
473
+ return "[#{value.map { js_literal(_1) }.join(', ')}]" if value.is_a?(Array)
474
+ if value.is_a?(Hash)
475
+ return "{ #{value.map { |key, item| "#{JSON.generate(key)}: #{js_literal(item)}" }.join(', ')} }"
476
+ end
477
+
478
+ JSON.generate(value)
479
+ end
480
+ end
481
+ # rubocop:enable Metrics
482
+ end
483
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module Compiler
5
+ # Compiles server-visible navigation profiles while preserving Runtime widget inventories.
6
+ class NavigationDocumentCompiler
7
+ include ModelValues
8
+
9
+ def initialize(schema)
10
+ @schema = schema
11
+ end
12
+
13
+ def compile(unit) # rubocop:disable Metrics/MethodLength
14
+ source = unit.document
15
+ existing = @schema.counterpart(source) || {}
16
+ @schema.fields_for(source).to_h do |field|
17
+ value = if field == 'Profiles'
18
+ array(source['Profiles']).map { profile(_1) }
19
+ elsif %w[$ID $Type].include?(field)
20
+ source[field]
21
+ else
22
+ existing.fetch(field, source.fetch(field, []))
23
+ end
24
+ [field, value]
25
+ end
26
+ end # rubocop:enable Metrics/MethodLength
27
+
28
+ private
29
+
30
+ def profile(source) # rubocop:disable Metrics/AbcSize
31
+ existing = @schema.counterpart(source) || {}
32
+ values = {
33
+ 'OfflineEntityConfigsRuntime' => offline_configs(source, existing),
34
+ 'HomePage' => home_page(source['HomePage']), 'HomeItems' => plain_array(source['HomeItems']),
35
+ 'AppTitle' => text_reference(source['AppTitle']),
36
+ 'LoginPageSettings' => form_settings(source['LoginPageSettings'])
37
+ }.merge(profile_settings(source, existing))
38
+ @schema.fields_for(source).to_h do |field|
39
+ [field, values.fetch(field, source.fetch(field, existing[field]))]
40
+ end
41
+ end # rubocop:enable Metrics/AbcSize
42
+
43
+ def profile_settings(source, existing)
44
+ {
45
+ 'ProgressiveWebAppSettings' => plain_document(source['ProgressiveWebAppSettings']),
46
+ 'NotFoundHomepage' => plain_document(source['NotFoundHomepage']),
47
+ 'Name' => source['Name'], 'IsOffline' => source['Kind'] == 'Offline',
48
+ 'ThrowPartialSyncError' => source['ThrowPartialSyncError'] == true,
49
+ 'Kind' => source['Kind'], 'AppIcon' => source.fetch('AppIcon', existing.fetch('AppIcon', ''))
50
+ }
51
+ end
52
+
53
+ def offline_configs(source, existing)
54
+ return existing['OfflineEntityConfigsRuntime'] if existing.key?('OfflineEntityConfigsRuntime')
55
+
56
+ plain_array(source['OfflineEntityConfigs'])
57
+ end
58
+
59
+ def home_page(source)
60
+ return nil unless source
61
+
62
+ {
63
+ '$ID' => source['$ID'], '$Type' => source['$Type'],
64
+ 'Page' => source['Page'].to_s, 'Microflow' => source['Microflow'].to_s
65
+ }
66
+ end
67
+
68
+ def form_settings(source)
69
+ return nil unless source
70
+
71
+ {
72
+ '$ID' => source['$ID'], '$Type' => source['$Type'],
73
+ 'TitleOverride' => text_reference(source['TitleOverride']),
74
+ 'ParameterMappings' => plain_array(source['ParameterMappings']),
75
+ 'Form' => source['Form'].to_s, 'Location' => source.fetch('Location', 'Content')
76
+ }
77
+ end
78
+
79
+ def text_reference(source)
80
+ return nil unless source
81
+
82
+ @schema.fields_for(source).to_h do |field|
83
+ value = case field
84
+ when 'Parameters' then array(source[field]).map { text_reference(_1) }
85
+ when 'Text' then text_reference(source[field])
86
+ else source[field]
87
+ end
88
+ [field, value]
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+ require 'zip'
6
+
7
+ module Mxrb
8
+ module Compiler
9
+ PackageResult = Data.define(:path, :mendix_version, :files, :sha256, :metadata)
10
+
11
+ # Pure-Ruby MDA container writer. This first compiler stage packages an
12
+ # already materialized deployment and never invokes mx or mxbuild.
13
+ class Packager
14
+ FIXED_TIME = Time.utc(2000, 1, 1).freeze
15
+
16
+ def initialize(mpr_path, deployment: nil)
17
+ @mpr_path = File.expand_path(mpr_path)
18
+ @project_root = File.dirname(@mpr_path)
19
+ @deployment = File.expand_path(deployment || File.join(@project_root, 'deployment'))
20
+ end
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)
32
+ end
33
+
34
+ private
35
+
36
+ def validate_input!(output, force:)
37
+ raise CompilationError, "#{output}: file already exists" if File.exist?(output) && !force
38
+ return if File.directory?(@deployment)
39
+
40
+ raise CompilationError, "#{@deployment}: deployment directory not found"
41
+ end
42
+
43
+ def package_result(output, version, metadata)
44
+ inspection = Mda.inspect(output)
45
+ PackageResult.new(
46
+ path: output, mendix_version: version, files: inspection.files.size,
47
+ sha256: inspection.sha256, metadata:
48
+ )
49
+ end
50
+
51
+ def inventory
52
+ paths = deployment_paths
53
+ symlink = paths.find { File.symlink?(_1) }
54
+ raise CompilationError, "deployment contains symlink #{symlink}" if symlink
55
+
56
+ files = paths.select { File.file?(_1) }.sort_by { relative(_1) }
57
+ directories = paths.select { File.directory?(_1) }.sort_by { relative(_1) }
58
+ [files, directories]
59
+ end
60
+
61
+ def deployment_paths
62
+ roots = Adapter::ROOTS.map { File.join(@deployment, _1) }.select { File.exist?(_1) }
63
+ roots.flat_map { Dir.glob(File.join(_1, '**', '*'), File::FNM_DOTMATCH) }
64
+ .concat(roots)
65
+ .reject { %w[. ..].include?(File.basename(_1)) }
66
+ end
67
+
68
+ def relative(path)
69
+ path.delete_prefix("#{@deployment}/").tr('\\', '/')
70
+ end
71
+
72
+ def write_atomically(output, files, directories)
73
+ FileUtils.mkdir_p(File.dirname(output))
74
+ Dir.mktmpdir('mxrb-mda-', File.dirname(output)) do |tmpdir|
75
+ temporary = File.join(tmpdir, 'package.mda')
76
+ Zip::File.open(temporary, create: true) do |archive|
77
+ directories.each { add_directory(archive, _1) }
78
+ files.each { add_file(archive, _1) }
79
+ end
80
+ FileUtils.mv(temporary, output, force: true)
81
+ end
82
+ end
83
+
84
+ def add_directory(archive, source)
85
+ name = "#{relative(source).delete_suffix('/')}/"
86
+ entry = Zip::Entry.new('', name)
87
+ entry.time = FIXED_TIME
88
+ entry.unix_perms = File.stat(source).mode & 0o777
89
+ archive.add(entry, source)
90
+ end
91
+
92
+ def add_file(archive, source)
93
+ entry = Zip::Entry.new('', relative(source))
94
+ entry.time = FIXED_TIME
95
+ entry.unix_perms = File.stat(source).mode & 0o777
96
+ archive.add(entry, source)
97
+ end
98
+ end
99
+ end
100
+ end