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,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Mxrb
6
+ module Compare
7
+ Change = Data.define(:operation, :path, :before, :after) do
8
+ def added? = operation == :added
9
+ def removed? = operation == :removed
10
+ def changed? = operation == :changed
11
+ end
12
+
13
+ Result = Struct.new(:differences, :changes, keyword_init: true) do
14
+ def identical? = differences.empty?
15
+ def added = changes.select(&:added?)
16
+ def removed = changes.select(&:removed?)
17
+ def changed = changes.select(&:changed?)
18
+ end
19
+
20
+ class Comparator
21
+ def initialize(left_path, right_path)
22
+ @left_path = left_path
23
+ @right_path = right_path
24
+ end
25
+
26
+ def compare
27
+ left = snapshot(@left_path)
28
+ right = snapshot(@right_path)
29
+ changes = diff_values(left, right)
30
+ Result.new(
31
+ differences: changes.map { format_change(_1) }.freeze,
32
+ changes: changes.freeze
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def snapshot(path)
39
+ Mxrb.open(path) do |project|
40
+ {
41
+ project: {
42
+ mendix_version: project.mendix_version,
43
+ format_version: project.format_version
44
+ },
45
+ security: security_summary(project),
46
+ navigation: normalize_hash(project.navigation.to_h),
47
+ design_assets: design_asset_summary(project),
48
+ units: unit_summary(project),
49
+ modules: project.modules.sort_by(&:name).map { module_summary(_1) }
50
+ }
51
+ end
52
+ end
53
+
54
+ def design_asset_summary(project)
55
+ root = File.dirname(project.mpr.path)
56
+ Model::DesignSystem::ASSET_DIRECTORIES.flat_map do |directory|
57
+ Dir.glob(File.join(root, directory, "**", "*"))
58
+ end.select { File.file?(_1) && !File.symlink?(_1) }.sort.to_h do |path|
59
+ relative = Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
60
+ [relative, Digest::SHA256.file(path).hexdigest]
61
+ end
62
+ end
63
+
64
+ def security_summary(project)
65
+ raw = project.all_units.find { project.parse_bson(_1)["$Type"] == "Security$ProjectSecurity" }
66
+ return nil unless raw
67
+
68
+ doc = project.parse_bson(raw)
69
+ {
70
+ security_level: doc["SecurityLevel"],
71
+ check_security: doc["CheckSecurity"],
72
+ admin_user_name: doc["AdminUserName"],
73
+ admin_user_role: doc["AdminUserRole"],
74
+ demo_users_enabled: doc["EnableDemoUsers"],
75
+ guest_access_enabled: doc["EnableGuestAccess"],
76
+ guest_user_role: doc["GuestUserRole"],
77
+ sign_in_microflow: doc["SignInMicroflow"],
78
+ password_policy: normalize_flow_value(doc["PasswordPolicySettings"], {}),
79
+ user_roles: IO::BsonCodec.parse_array(doc["UserRoles"]).fetch(:items).map do |role|
80
+ {
81
+ name: role["Name"],
82
+ admin: role["ManageAllRoles"] == true,
83
+ module_roles: IO::BsonCodec.parse_array(role["ModuleRoles"]).fetch(:items).sort
84
+ }
85
+ end.sort_by { _1[:name].to_s }
86
+ }
87
+ end
88
+
89
+ def unit_summary(project)
90
+ project.all_units.map do |unit|
91
+ next if unit["UnitID"] == unit["ContainerID"]
92
+
93
+ doc = project.parse_bson(unit)
94
+ {
95
+ containment: unit["ContainmentName"].to_s,
96
+ container_root: false,
97
+ type: doc["$Type"].to_s,
98
+ name: doc["Name"] || doc["name"] || ""
99
+ }
100
+ end.compact.sort_by { [_1[:containment], 1, _1[:type], _1[:name]] }
101
+ end
102
+
103
+ def module_summary(mod)
104
+ {
105
+ name: mod.name,
106
+ entities: mod.entities.sort_by(&:name).map { entity_summary(_1) },
107
+ associations: mod.associations.sort_by(&:name).map { association_summary(_1) },
108
+ pages: mod.pages.sort_by(&:name).map { page_summary(_1) },
109
+ menus: mod.menus.sort_by(&:name).map { menu_summary(_1) },
110
+ module_roles: mod.module_roles.sort_by { _1[:name].to_s },
111
+ microflows: mod.microflows.sort_by(&:name).map { flow_summary(_1) },
112
+ nanoflows: mod.nanoflows.sort_by(&:name).map { flow_summary(_1) }
113
+ }
114
+ end
115
+
116
+ def entity_summary(entity)
117
+ {
118
+ name: entity.name,
119
+ documentation: entity.documentation.to_s,
120
+ persistable: entity.persistable != false,
121
+ generalization: normalize_generalization(entity.generalization),
122
+ access_rules: Array(entity.access_rules).map { normalize_hash(_1) },
123
+ attributes: entity.attributes.sort_by(&:name).map do |attribute|
124
+ {
125
+ name: attribute.name,
126
+ documentation: attribute.documentation.to_s,
127
+ type: attribute.type,
128
+ default: attribute.default_value.to_s,
129
+ native_type: normalize_flow_value(attribute.raw_type_doc, {}),
130
+ native_value: normalize_flow_value(attribute.raw_value_doc, {})
131
+ }
132
+ end
133
+ }
134
+ end
135
+
136
+ def association_summary(association)
137
+ {
138
+ name: association.name,
139
+ type: association.association_type,
140
+ owner: association.owner,
141
+ storage_format: association.storage_format,
142
+ documentation: association.documentation.to_s,
143
+ delete_behavior: normalize_flow_value(association.delete_behavior, {})
144
+ }
145
+ end
146
+
147
+ def page_summary(page)
148
+ {
149
+ name: page.name,
150
+ title: page.title,
151
+ layout: page.layout_id,
152
+ allowed_roles: page.allowed_module_roles.sort,
153
+ data_source: normalize_hash(page.data_source),
154
+ widgets: page.widgets.map { widget_summary(_1) }
155
+ }
156
+ end
157
+
158
+ def menu_summary(menu)
159
+ {
160
+ name: menu.name,
161
+ items: menu.items.map { menu_item_summary(_1) }
162
+ }
163
+ end
164
+
165
+ def menu_item_summary(item)
166
+ {
167
+ name: item[:caption],
168
+ caption: item[:caption],
169
+ page: item[:page],
170
+ items: Array(item[:items]).map { menu_item_summary(_1) }
171
+ }
172
+ end
173
+
174
+ def widget_summary(widget)
175
+ {
176
+ type: widget[:type],
177
+ name: widget[:name],
178
+ options: normalize_hash(widget.fetch(:options, {})),
179
+ events: Array(widget[:events]).map { normalize_hash(_1) },
180
+ children: Array(widget[:children]).map { widget_summary(_1) }
181
+ }
182
+ end
183
+
184
+ def flow_summary(flow)
185
+ ids = {}
186
+ assign_flow_ids(flow.objects, flow.flows, ids)
187
+ objects = flow.objects.sort_by { ids.fetch(_1["$ID"], ids.size) }
188
+ normalized_flows = flow.flows.map do |edge|
189
+ {
190
+ origin: normalize_flow_value(edge["OriginPointer"], ids),
191
+ destination: normalize_flow_value(edge["DestinationPointer"], ids),
192
+ error_handler: edge["IsErrorHandler"] == true,
193
+ cases: normalized_case_values(edge, ids)
194
+ }
195
+ end.sort_by { [_1[:origin].to_s, _1[:destination].to_s, _1[:error_handler].to_s, _1[:cases].to_s] }
196
+ {
197
+ name: flow.name,
198
+ return_type: flow.return_type.to_s,
199
+ allowed_roles: flow.allowed_module_roles.sort,
200
+ parameters: flow.parameters.map { normalize_hash(_1) },
201
+ body: {
202
+ objects: objects.map { normalize_flow_value(_1, ids) },
203
+ flows: normalized_flows
204
+ }
205
+ }
206
+ end
207
+
208
+ def normalize_flow_value(value, ids)
209
+ return { object: ids.fetch(value) } if ids.key?(value)
210
+
211
+ case value
212
+ when Hash
213
+ value.each_with_object({}) do |(key, item), result|
214
+ next if %w[
215
+ $ID X Y RelativeMiddlePoint Size
216
+ OriginBezierVector DestinationBezierVector
217
+ OriginConnectionIndex DestinationConnectionIndex Line
218
+ ].include?(key.to_s)
219
+ next if key.to_s.end_with?("Model") &&
220
+ item.is_a?(Hash) && item["$Type"] == "Expressions$NoExpression"
221
+
222
+ result[key.to_sym] = normalize_flow_value(item, ids)
223
+ end
224
+ when Array
225
+ items = value
226
+ if items.all? { _1.is_a?(Hash) && ids.key?(_1["$ID"]) }
227
+ items = items.sort_by { ids.fetch(_1["$ID"]) }
228
+ end
229
+ items.map { normalize_flow_value(_1, ids) }
230
+ else
231
+ value
232
+ end
233
+ end
234
+
235
+ def assign_flow_ids(objects, flows, ids)
236
+ local_ids = objects.to_h { [_1["$ID"], true] }
237
+ local_flows = flows.select do |edge|
238
+ local_ids.key?(edge["OriginPointer"]) && local_ids.key?(edge["DestinationPointer"])
239
+ end
240
+ incoming = local_flows.group_by { _1["DestinationPointer"] }
241
+ outgoing = local_flows.group_by { _1["OriginPointer"] }
242
+ roots = objects.select { _1["$Type"] == "Microflows$StartEvent" }
243
+ roots += objects.reject { incoming.key?(_1["$ID"]) || roots.include?(_1) }
244
+ queue = roots
245
+ until queue.empty?
246
+ object = queue.shift
247
+ next if ids.key?(object["$ID"])
248
+
249
+ ids[object["$ID"]] = ids.size
250
+ nested = object.dig("ObjectCollection", "Objects")
251
+ assign_flow_ids(array_payload(nested), flows, ids) if nested
252
+ edges = Array(outgoing[object["$ID"]]).sort_by do |edge|
253
+ [edge["IsErrorHandler"] == true ? 1 : 0, normalized_case_values(edge, ids).to_s]
254
+ end
255
+ edges.each do |edge|
256
+ target = objects.find { _1["$ID"] == edge["DestinationPointer"] }
257
+ queue << target
258
+ end
259
+ end
260
+ objects.each do |object|
261
+ next if ids.key?(object["$ID"])
262
+
263
+ ids[object["$ID"]] = ids.size
264
+ nested = object.dig("ObjectCollection", "Objects")
265
+ assign_flow_ids(array_payload(nested), flows, ids) if nested
266
+ end
267
+ end
268
+
269
+ def normalized_case_values(edge, ids)
270
+ values = if edge["CaseValues"]
271
+ array_payload(edge["CaseValues"])
272
+ elsif edge["NewCaseValue"]
273
+ [edge["NewCaseValue"]]
274
+ else
275
+ []
276
+ end
277
+ values.filter_map do |value|
278
+ next if value["$Type"].to_s.end_with?("$NoCase")
279
+
280
+ normalize_flow_value(value.reject { |key, _| key == "$ID" }, ids)
281
+ end
282
+ end
283
+
284
+ def array_payload(value)
285
+ IO::BsonCodec.parse_array(value)[:items]
286
+ rescue StandardError
287
+ []
288
+ end
289
+
290
+ def diff_values(left, right, path = [])
291
+ return [] if left == right
292
+
293
+ if left.is_a?(Hash) && right.is_a?(Hash)
294
+ keys = (left.keys + right.keys).uniq.sort_by(&:to_s)
295
+ return keys.flat_map { diff_values(left[_1], right[_1], path + [_1]) }
296
+ end
297
+
298
+ if left.is_a?(Array) && right.is_a?(Array)
299
+ return diff_named_arrays(left, right, path) if named_array?(left) && named_array?(right)
300
+
301
+ max = [left.size, right.size].max
302
+ return (0...max).flat_map { diff_values(left[_1], right[_1], path + [_1]) }
303
+ end
304
+
305
+ [Change.new(change_operation(left, right), path.freeze, left, right)]
306
+ end
307
+
308
+ def diff_named_arrays(left, right, path)
309
+ left_by_name = left.to_h { [_1[:name], _1] }
310
+ right_by_name = right.to_h { [_1[:name], _1] }
311
+ common_names = (left_by_name.keys & right_by_name.keys).sort
312
+ changes = common_names.flat_map do |name|
313
+ diff_values(left_by_name[name], right_by_name[name], path + [name])
314
+ end
315
+
316
+ left_only = (left_by_name.keys - common_names).sort.map { left_by_name.fetch(_1) }
317
+ right_only = (right_by_name.keys - common_names).sort.map { right_by_name.fetch(_1) }
318
+ left_only.dup.each do |left_item|
319
+ match = right_only.find { same_except_name?(left_item, _1) }
320
+ next unless match
321
+
322
+ changes.concat(diff_values(left_item, match, path + [left_item[:name]]))
323
+ left_only.delete(left_item)
324
+ right_only.delete(match)
325
+ end
326
+
327
+ changes.concat(left_only.flat_map { diff_values(_1, nil, path + [_1[:name]]) })
328
+ changes.concat(right_only.flat_map { diff_values(nil, _1, path + [_1[:name]]) })
329
+ changes
330
+ end
331
+
332
+ def named_array?(items)
333
+ return false unless items.all? { _1.is_a?(Hash) && _1.key?(:name) }
334
+
335
+ names = items.map { _1[:name] }
336
+ names.uniq.size == names.size
337
+ end
338
+
339
+ def same_except_name?(left, right)
340
+ left.reject { |key, _| key == :name } == right.reject { |key, _| key == :name }
341
+ end
342
+
343
+ def normalize_hash(value)
344
+ return value unless value.is_a?(Hash)
345
+
346
+ value.to_h do |key, item|
347
+ normalized = if item.is_a?(Hash)
348
+ normalize_hash(item)
349
+ elsif item.is_a?(Array)
350
+ item.map { _1.is_a?(Hash) ? normalize_hash(_1) : _1 }
351
+ else
352
+ item
353
+ end
354
+ [key.to_sym, normalized]
355
+ end
356
+ end
357
+
358
+ def normalize_generalization(value)
359
+ defaults = {
360
+ Persistable: false, HasCreatedDateAttr: false, HasChangedDateAttr: false,
361
+ HasOwnerAttr: false, HasChangedByAttr: false
362
+ }
363
+ defaults.merge(normalize_flow_value(value, {}))
364
+ end
365
+
366
+ def format_path(path)
367
+ path.map { _1.is_a?(Integer) ? "[#{_1}]" : _1 }.join(".")
368
+ end
369
+
370
+ def change_operation(left, right)
371
+ return :added if left.nil?
372
+ return :removed if right.nil?
373
+
374
+ :changed
375
+ end
376
+
377
+ def format_change(change)
378
+ "#{format_path(change.path)}: #{change.before.inspect} != #{change.after.inspect}"
379
+ end
380
+ end
381
+ end
382
+
383
+ def self.compare(left_path, right_path)
384
+ Compare::Comparator.new(left_path, right_path).compare
385
+ end
386
+
387
+ class << self
388
+ alias diff compare
389
+ end
390
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Mxrb
6
+ module Compiler
7
+ # Version-specific deployment contract. Compiler stages dispatch through
8
+ # this object instead of scattering major-version checks through the code.
9
+ class Adapter
10
+ REQUIRED_FILES = %w[
11
+ model/model.mdp
12
+ model/metadata.json
13
+ model/bundles/project.jar
14
+ web/index.html
15
+ ].freeze
16
+ ROOTS = %w[model web native sass tmp].freeze
17
+
18
+ attr_reader :version, :major
19
+
20
+ def self.for(version, source: nil)
21
+ major = version.to_s.split('.').first.to_i
22
+ klass = { 6 => V6, 7 => V7, 9 => V9, 10 => V10, 11 => V11 }[major]
23
+ unless klass
24
+ raise UnsupportedVersion,
25
+ "audited native compilation supports Mendix 6.x, 7.x, 9.x, 10.x, and 11.x; got #{version}"
26
+ end
27
+
28
+ klass.new(version, source:)
29
+ end
30
+
31
+ def initialize(version, source: nil)
32
+ @version = version.to_s
33
+ @major = @version.split('.').first.to_i
34
+ @source = source
35
+ end
36
+
37
+ def validate_deployment!(root)
38
+ validate_required_files!(root)
39
+ metadata = read_metadata(root)
40
+ runtime_version = metadata.fetch('RuntimeVersion').to_s
41
+ return metadata if compatible_runtime?(runtime_version)
42
+
43
+ raise CompilationError,
44
+ "deployment targets Mendix #{runtime_version}, but MPR targets #{version}"
45
+ rescue JSON::ParserError, KeyError => e
46
+ raise CompilationError, "invalid model/metadata.json: #{e.message}"
47
+ end
48
+
49
+ def validate_freshness!(mpr_path, root)
50
+ compiled_model = File.join(root, 'model', 'model.mdp')
51
+ return if File.mtime(compiled_model) >= File.mtime(mpr_path)
52
+
53
+ raise CompilationError,
54
+ "deployment is stale: #{compiled_model} is older than #{File.expand_path(mpr_path)}"
55
+ end
56
+
57
+ def validate_required_files!(root)
58
+ missing = REQUIRED_FILES.reject { File.file?(File.join(root, _1)) }
59
+ return if missing.empty?
60
+
61
+ raise CompilationError, "deployment is not materialized; missing #{missing.join(', ')}"
62
+ end
63
+
64
+ def read_metadata(root)
65
+ JSON.parse(File.read(File.join(root, 'model', 'metadata.json')))
66
+ end
67
+
68
+ def compatible_runtime?(runtime_version)
69
+ target = version.split('.').first(3)
70
+ actual = runtime_version.split('.').first(3)
71
+ target == actual
72
+ end
73
+
74
+ def web_profiles = [:dojo]
75
+
76
+ class V6 < Adapter; end
77
+ class V7 < Adapter; end
78
+
79
+ # Hybrid Dojo client with the React-wrapper bridge introduced in Mendix 9.
80
+ class V9 < Adapter
81
+ def web_profiles = %i[dojo react_wrapper]
82
+ end
83
+
84
+ # Mendix 10 owns both the classic Dojo client and the opt-in optimized
85
+ # React client. The selection is stored in the project model.
86
+ class V10 < Adapter
87
+ def web_profiles = @source&.optimized_web_client? ? [:react] : [:dojo]
88
+ end
89
+
90
+ # Current fully React web client contract.
91
+ class V11 < Adapter
92
+ def web_profiles = [:react]
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module Compiler
5
+ # Compiles Runtime roots whose executable representation is independent of UI bundles.
6
+ class ArtifactDocumentCompiler
7
+ include ModelValues
8
+
9
+ NAME_ONLY_TYPES = %w[
10
+ CustomIcons$CustomIconCollection Forms$Layout Forms$Snippet Menus$MenuDocument
11
+ ].freeze
12
+ TYPES = (NAME_ONLY_TYPES + %w[
13
+ Enumerations$Enumeration Images$ImageCollection RegularExpressions$RegularExpression
14
+ ScheduledEvents$ScheduledEvent DomainModels$ViewEntitySourceDocument
15
+ ]).freeze
16
+
17
+ def compile(unit) # rubocop:disable Metrics/AbcSize
18
+ source = unit.document
19
+ return named(source, unit.module_name) if NAME_ONLY_TYPES.include?(source['$Type'])
20
+
21
+ case source['$Type']
22
+ when 'Enumerations$Enumeration' then enumeration(source, unit.module_name)
23
+ when 'Images$ImageCollection' then image_collection(source, unit.module_name)
24
+ when 'RegularExpressions$RegularExpression' then regular_expression(source, unit.module_name)
25
+ when 'ScheduledEvents$ScheduledEvent' then scheduled_event(source, unit.module_name)
26
+ when 'DomainModels$ViewEntitySourceDocument' then named(source, unit.module_name)
27
+ else raise CompilationError, "unsupported Runtime artifact #{source['$Type']}"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def named(source, module_name)
34
+ {
35
+ '$ID' => source['$ID'], '$Type' => source['$Type'], 'Name' => source['Name'],
36
+ 'QualifiedName' => qualified_name(module_name, source['Name'])
37
+ }
38
+ end
39
+
40
+ def enumeration(source, module_name)
41
+ {
42
+ '$ID' => source['$ID'], '$Type' => source['$Type'],
43
+ 'Values' => array(source['Values']).map { enumeration_value(_1) },
44
+ 'Name' => source['Name'], 'QualifiedName' => qualified_name(module_name, source['Name'])
45
+ }
46
+ end
47
+
48
+ def enumeration_value(source)
49
+ {
50
+ '$ID' => source['$ID'], '$Type' => source['$Type'],
51
+ 'Caption' => text_reference(source['Caption']), 'RemoteValue' => source['RemoteValue'],
52
+ 'Name' => source['Name'], 'Image' => source['Image'].to_s
53
+ }
54
+ end
55
+
56
+ def image_collection(source, module_name)
57
+ collection_name = qualified_name(module_name, source['Name'])
58
+ {
59
+ '$ID' => source['$ID'], '$Type' => source['$Type'],
60
+ 'Images' => array(source['Images']).map { image(_1, collection_name) },
61
+ 'Name' => source['Name'], 'QualifiedName' => collection_name
62
+ }
63
+ end
64
+
65
+ def image(source, collection_name)
66
+ {
67
+ '$ID' => source['$ID'], '$Type' => source['$Type'], 'Name' => source['Name'],
68
+ 'Image' => source['Image'], 'Format' => image_format(source),
69
+ 'QualifiedName' => "#{collection_name}.#{source['Name']}"
70
+ }
71
+ end
72
+
73
+ def regular_expression(source, module_name)
74
+ named(source, module_name).merge('Expression' => source['Expression'].to_s)
75
+ end
76
+
77
+ def scheduled_event(source, module_name)
78
+ {
79
+ '$ID' => source['$ID'], '$Type' => source['$Type'],
80
+ 'Schedule' => plain_document(source['Schedule']), 'Name' => source['Name'],
81
+ 'QualifiedName' => qualified_name(module_name, source['Name']),
82
+ 'StartDateTime' => source['StartDateTime'], 'TimeZone' => source['TimeZone'],
83
+ 'OnOverlap' => source['OnOverlap'], 'Interval' => source['Interval'],
84
+ 'IntervalType' => source['IntervalType'], 'Microflow' => source['Microflow'],
85
+ 'Documentation' => source['Documentation'].to_s
86
+ }
87
+ end
88
+
89
+ def text_reference(source)
90
+ return nil unless source
91
+
92
+ source.slice('$ID', '$Type')
93
+ end
94
+
95
+ def qualified_name(module_name, name)
96
+ raise CompilationError, "artifact #{name} is outside a module" unless module_name
97
+
98
+ "#{module_name}.#{name}"
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Mxrb
6
+ module Compiler
7
+ ArtifactMaterialization = Data.define(:model_path, :metadata_path, :documents, :scheduled_events)
8
+
9
+ # Materializes executable non-flow artifacts and their Runtime metadata.
10
+ class ArtifactMaterializer
11
+ def initialize(mpr_path, deployment:)
12
+ @mpr_path = File.expand_path(mpr_path)
13
+ @deployment = File.expand_path(deployment)
14
+ end
15
+
16
+ def materialize
17
+ source = SourceModel.read(@mpr_path)
18
+ model_path = File.join(@deployment, 'model', 'model.mdp')
19
+ metadata_path = File.join(@deployment, 'model', 'metadata.json')
20
+ documents = compile_documents(source)
21
+ write_model(model_path, documents)
22
+ events = write_metadata(metadata_path, source)
23
+ ArtifactMaterialization.new(
24
+ model_path:, metadata_path:, documents: documents.length, scheduled_events: events.length
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def compile_documents(source)
31
+ compiler = ArtifactDocumentCompiler.new
32
+ source.units.select { ArtifactDocumentCompiler::TYPES.include?(_1.document['$Type']) }
33
+ .map { compiler.compile(_1) }
34
+ end
35
+
36
+ def write_model(path, documents)
37
+ documents.reduce(ModelPackage.read(path)) { |package, document| package.upsert(document) }
38
+ .write(path)
39
+ end
40
+
41
+ def write_metadata(path, source)
42
+ events = source.units_of('ScheduledEvents$ScheduledEvent').map do |unit|
43
+ { 'Name' => "#{unit.module_name}.#{unit.document['Name']}",
44
+ 'Description' => unit.document['Documentation'].to_s }
45
+ end
46
+ metadata = JSON.parse(File.read(path))
47
+ metadata['ScheduledEvents'] = events
48
+ File.write(path, JSON.pretty_generate(metadata))
49
+ events
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module Compiler
5
+ ClientModelMaterialization = Data.define(:model_path, :pages, :navigation_documents)
6
+
7
+ # Materializes Runtime page descriptors and navigation documents.
8
+ class ClientModelMaterializer
9
+ TYPES = %w[Forms$Page Navigation$NavigationDocument].freeze
10
+
11
+ def initialize(mpr_path, deployment:)
12
+ @mpr_path = File.expand_path(mpr_path)
13
+ @deployment = File.expand_path(deployment)
14
+ end
15
+
16
+ def materialize
17
+ source = SourceModel.read(@mpr_path)
18
+ path = File.join(@deployment, 'model', 'model.mdp')
19
+ package = ModelPackage.read(path)
20
+ pages = source.units_of('Forms$Page').map { PageDocumentCompiler.new(source).compile(_1) }
21
+ navigation = compile_navigation(source, package)
22
+ write(path, package, pages + navigation)
23
+ ClientModelMaterialization.new(
24
+ model_path: path, pages: pages.length, navigation_documents: navigation.length
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def compile_navigation(source, package)
31
+ compiler = NavigationDocumentCompiler.new(
32
+ RuntimeModelSchema.new(package, version: source.version)
33
+ )
34
+ source.units_of('Navigation$NavigationDocument').map { compiler.compile(_1) }
35
+ end
36
+
37
+ def write(path, package, documents)
38
+ documents.reduce(package) { |result, document| result.upsert(document) }.write(path)
39
+ end
40
+ end
41
+ end
42
+ end