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,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Mxrb
6
+ module Compiler
7
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength
8
+ # Compiles the built-in Data Grid 1 widget to the Dojo web-client contract.
9
+ class LegacyDataGridCompiler
10
+ include ModelValues
11
+
12
+ SOURCE_TYPES = {
13
+ 'Forms$GridDatabaseSource' => 'database',
14
+ 'Forms$NewGridDatabaseSource' => 'database',
15
+ 'Forms$GridXPathSource' => 'xpath',
16
+ 'Forms$MicroflowSource' => 'microflow'
17
+ }.freeze
18
+ BUTTON_FUNCTIONS = {
19
+ 'Forms$GridSearchButton' => 'ToggleSearch',
20
+ 'Forms$DataGridSelectButton' => 'ReturnSelection',
21
+ 'Forms$GridNewButton' => 'InsertNew',
22
+ 'Forms$GridEditButton' => 'EditSelection',
23
+ 'Forms$GridDeleteButton' => 'DeleteSelection',
24
+ 'Forms$GridActionButton' => 'InvokeAction'
25
+ }.freeze
26
+
27
+ attr_reader :unsupported
28
+
29
+ def initialize(source, page_name, widget, language:, sequence:)
30
+ @source = source
31
+ @page_name = page_name
32
+ @widget = widget
33
+ @language = language
34
+ @sequence = sequence
35
+ @unsupported = []
36
+ end
37
+
38
+ def html
39
+ props = JSON.generate(properties)[1..-2]
40
+ attributes = {
41
+ 'data-mendix-id' => widget_id, 'data-mendix-type' => 'mxui.widget.DataGrid',
42
+ 'data-mendix-props' => props, 'class' => css_class, 'tabindex' => @widget['TabIndex'].to_i
43
+ }
44
+ "<div #{attributes.map { |key, value| "#{key}='#{escape(value)}'" }.join(' ')}></div>"
45
+ end
46
+
47
+ private
48
+
49
+ def properties
50
+ {
51
+ 'friendlyId' => friendly_id, 'entity' => entity, 'schema' => entity_schema,
52
+ 'config' => {
53
+ 'locale' => @language, 'gridType' => 'Standalone',
54
+ 'gridpresentation' => presentation, 'datasource' => datasource,
55
+ 'searchOptions' => search_options, 'searchElements' => search_elements,
56
+ 'controlBar' => control_bar, 'defaultButton' => default_button,
57
+ 'griddata' => columns.map { compile_column(_1) }, 'plugins' => {}
58
+ }
59
+ }
60
+ end
61
+
62
+ def presentation
63
+ {
64
+ 'rows' => @widget['NumberOfRows'].to_i, 'columns' => 1,
65
+ 'controlbar' => @widget['IsControlBarVisible'] == true,
66
+ 'pagingbar' => @widget['IsPagingEnabled'] == true,
67
+ 'sortparams' => sort_items, 'searchbar' => !search_elements.empty?,
68
+ 'waitforsearch' => data_source.dig('SearchBar', 'WaitForSearch') == true,
69
+ 'selectionmode' => @widget['SelectionMode'].to_s.downcase,
70
+ 'editable' => columns.any? { _1['Editable'] == true }, 'sortable' => true,
71
+ 'refresh' => @widget['RefreshTime'].to_i, 'selectfirst' => @widget['SelectFirst'] == true,
72
+ 'showemptyrows' => @widget['ShowEmptyRows'] == true
73
+ }
74
+ end
75
+
76
+ def datasource
77
+ type = SOURCE_TYPES[data_source['$Type']]
78
+ @unsupported << data_source['$Type'].to_s unless type
79
+ result = { 'friendlyId' => friendly_id, 'type' => type || 'unsupported', 'path' => entity }
80
+ constraint = data_source['XPathConstraint'].to_s
81
+ result['xpathConstraints'] = constraint if type == 'database' || !constraint.empty?
82
+ result['offlineConstraints'] = [] if type == 'database'
83
+ result['microflow'] = data_source.dig('MicroflowSettings', 'Microflow').to_s if type == 'microflow'
84
+ result
85
+ end
86
+
87
+ def search_options
88
+ kind = data_source.dig('SearchBar', 'Type').to_s
89
+ {
90
+ 'searchBar' => !search_elements.empty?, 'toggleable' => kind.start_with?('Foldable'),
91
+ 'toggledByDefault' => kind != 'FoldableOpen'
92
+ }
93
+ end
94
+
95
+ def search_elements
96
+ array(data_source.dig('SearchBar', 'NewButtons')).filter_map do |field|
97
+ unless %w[Forms$ComparisonSearchField Forms$DropDownSearchField].include?(field['$Type'])
98
+ @unsupported << field['$Type'].to_s
99
+ next
100
+ end
101
+
102
+ {
103
+ 'searchInputName' => field['Name'].to_s, 'defaults' => default_search_value(field),
104
+ 'defaultsParser' => 'Simple', 'caption' => translated(field['Caption']),
105
+ 'path' => relative_attribute(attribute_path(field)),
106
+ 'operator' => field.fetch('Operator', 'Equal').to_s.downcase
107
+ }
108
+ end
109
+ end
110
+
111
+ def control_bar
112
+ tools = buttons.each_with_index.map { |button, index| compile_button(button, index) }
113
+ {
114
+ 'gridTools' => tools.map(&:first),
115
+ 'gridActions' => tools.filter_map(&:last)
116
+ }
117
+ end
118
+
119
+ def compile_button(button, index)
120
+ id = "#{@sequence}_#{index}"
121
+ type = button['$Type'].to_s
122
+ function = BUTTON_FUNCTIONS[type]
123
+ @unsupported << type unless function
124
+ tool = {
125
+ 'mxid' => id, 'friendlyId' => "#{@page_name}.#{button['Name']}",
126
+ 'name' => button['Name'].to_s,
127
+ 'caption' => { '$type' => 'textTemplate', 'text' => button_caption(button),
128
+ 'friendlyId' => "#{@page_name}.#{button['Name']}" },
129
+ 'title' => translated(button['Tooltip']), 'cssClass' => button_class(button)
130
+ }
131
+ action = function && { 'keyValue' => id, 'gridFunction' => function }
132
+ params = button_params(button)
133
+ action['params'] = params if action && params
134
+ [tool, action]
135
+ end
136
+
137
+ def button_params(button)
138
+ case button['$Type']
139
+ when 'Forms$GridNewButton', 'Forms$GridEditButton'
140
+ { entity => { 'pageSettings' => page_settings(button['FormSettings']) } }
141
+ when 'Forms$GridActionButton'
142
+ { entity => { 'action' => client_action(button['Action']),
143
+ 'maintainSelection' => button['MaintainSelectionAfterMicroflow'] == true } }
144
+ end
145
+ end
146
+
147
+ def client_action(action)
148
+ case action&.fetch('$Type', nil)
149
+ when 'Forms$MicroflowAction'
150
+ microflow = action.dig('MicroflowSettings', 'Microflow').to_s
151
+ { 'type' => 'callMicroflow', 'hasParameter' => true,
152
+ 'params' => { 'name' => microflow, 'validate' => 'view', 'applyTo' => 'selection' } }
153
+ when 'Forms$FormAction'
154
+ { 'type' => 'openPage', 'params' => page_settings(action['FormSettings']) }
155
+ when 'Forms$DeleteClientAction'
156
+ { 'type' => 'deleteObject', 'params' => { 'applyTo' => 'selection' } }
157
+ when 'Forms$NoAction', nil then { 'type' => 'noAction' }
158
+ else
159
+ @unsupported << action['$Type'].to_s
160
+ { 'type' => 'unsupported' }
161
+ end
162
+ end
163
+
164
+ def page_settings(settings)
165
+ form = settings&.fetch('Form', '').to_s
166
+ location = settings&.fetch('Location', '').to_s
167
+ result = {
168
+ 'path' => "#{form.tr('.', '/')}.page.xml",
169
+ 'location' => %w[Popup ModalPopup].include?(location) ? 'modal' : 'content'
170
+ }
171
+ target = @source.units_of('Forms$Page').find do |unit|
172
+ "#{unit.module_name}.#{unit.document['Name']}" == form
173
+ end
174
+ result['resizable'] = true if target&.document&.fetch('PopupResizable', false) == true
175
+ result
176
+ end
177
+
178
+ def default_button
179
+ pointer = IO::BsonCodec.extract_id(@widget.dig('ControlBar', 'DefaultButtonPointer'))
180
+ index = buttons.index { IO::BsonCodec.extract_id(_1['$ID']) == pointer }
181
+ return nil unless index
182
+
183
+ { 'clickType' => @widget['DefaultButtonTrigger'].to_s.downcase, 'mxid' => "#{@sequence}_#{index}" }
184
+ end
185
+
186
+ def compile_column(column)
187
+ path = relative_attribute(attribute_path(column))
188
+ {
189
+ 'tag' => path, 'name' => column['Name'].to_s, 'editable' => column['Editable'] == true,
190
+ 'render' => render_type(column),
191
+ 'display' => { 'width' => "#{column['WidthValue'].to_i}%", 'string' => translated(column['Caption']) }
192
+ }
193
+ end
194
+
195
+ def sort_items
196
+ array(data_source.dig('SortBar', 'SortItems')).map do |item|
197
+ [relative_attribute(attribute_path(item)), item['SortOrder'].to_s.downcase.sub('ending', '')]
198
+ end
199
+ end
200
+
201
+ def entity
202
+ direct = data_source['EntityPath'] || data_source.dig('EntityRef', 'Entity')
203
+ direct.to_s.empty? ? attribute_path(columns.first).to_s.rpartition('.').first : direct.to_s
204
+ end
205
+
206
+ def entity_schema
207
+ IO::BsonCodec.extract_id(@widget['$ID']).to_s
208
+ end
209
+
210
+ def render_type(column)
211
+ format = column.dig('FormattingInfo', 'EnumFormat')
212
+ format == 'Image' ? 'EnumImage' : 'String'
213
+ end
214
+
215
+ def attribute_path(value)
216
+ value['AttributePath'] || value.dig('AttributeRef', 'Attribute')
217
+ end
218
+
219
+ def relative_attribute(path)
220
+ path.to_s.delete_prefix("#{entity}.")
221
+ end
222
+
223
+ def buttons
224
+ @buttons ||= begin
225
+ control = @widget['ControlBar'] || {}
226
+ [control['SearchButton'], *array(control['NewButtons'])].compact
227
+ end
228
+ end
229
+
230
+ def data_source = @widget['DataSource'] || {}
231
+ def columns = array(@widget['Columns'])
232
+ def friendly_id = "#{@page_name}.#{@widget['Name']}"
233
+ def widget_id = "#{@sequence}_#{buttons.length}"
234
+
235
+ def css_class
236
+ ['mx-datagrid', "mx-name-#{@widget['Name']}", @widget['Class']]
237
+ .map(&:to_s).reject(&:empty?).uniq.join(' ')
238
+ end
239
+
240
+ def translated(text)
241
+ items = array(text&.fetch('Items', nil))
242
+ items.find { _1['LanguageCode'] == @language }&.fetch('Text', '') ||
243
+ items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') ||
244
+ items.first&.fetch('Text', '') || ''
245
+ end
246
+
247
+ def button_caption(button)
248
+ translated(button.dig('CaptionTemplate', 'Template'))
249
+ end
250
+
251
+ def button_class(button)
252
+ style = button['ButtonStyle'].to_s.downcase
253
+ "btn-#{style.empty? ? 'default' : style}"
254
+ end
255
+
256
+ def default_search_value(field)
257
+ value = field['DefaultValue']
258
+ value.nil? || value.to_s.empty? ? nil : value
259
+ end
260
+
261
+ def escape(value)
262
+ value.to_s.gsub('&', '&amp;').gsub("'", '&#39;').gsub('<', '&lt;').gsub('>', '&gt;')
263
+ end
264
+ end
265
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength
266
+ end
267
+ end
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module Mxrb
7
+ module Compiler
8
+ LegacyPageBuild = Data.define(:directory, :files, :bytes, :unsupported_widgets)
9
+
10
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
11
+ # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
12
+ # Writes deterministic Dojo page XML for the built-in Data Grid 1 contract.
13
+ class LegacyPageBuilder
14
+ include ModelValues
15
+
16
+ # Model elements that own visible client widgets. Supporting a page means
17
+ # rendering these elements, not merely serializing their nested settings.
18
+ VISUAL_WIDGET_TYPES = %w[
19
+ Forms$ActionButton Forms$CheckBox Forms$DataGrid Forms$DataView Forms$DatePicker
20
+ Forms$DivContainer Forms$DropDown Forms$DynamicText Forms$ImageViewer Forms$Label
21
+ Forms$LayoutGrid Forms$ListView Forms$RadioButtonGroup Forms$ReferenceSelector
22
+ Forms$SnippetCallWidget Forms$StaticImageViewer Forms$TabControl Forms$TextArea
23
+ Forms$TextBox Forms$InputReferenceSetSelector CustomWidgets$CustomWidget
24
+ ].freeze
25
+
26
+ def initialize(source, web_root, profiles: [:dojo])
27
+ @source = source
28
+ @web_root = web_root
29
+ @profiles = profiles
30
+ @unsupported = Hash.new { |hash, key| hash[key] = [] }
31
+ end
32
+
33
+ def build
34
+ pages = @source.units_of('Forms$Page')
35
+ layouts = @source.units_of('Forms$Layout')
36
+ languages.each do |language|
37
+ layouts.each { write_layout(_1, language) }
38
+ pages.each { write_page(_1, language) }
39
+ end
40
+ write_manifest
41
+ files = Dir.glob(File.join(@web_root, 'pages', '**', '*.xml')).select { File.file?(_1) }
42
+ LegacyPageBuild.new(
43
+ directory: File.join(@web_root, 'pages'), files: files.length,
44
+ bytes: files.sum { File.size(_1) }, unsupported_widgets: frozen_unsupported
45
+ )
46
+ end
47
+
48
+ # Performs the same widget audit as #build without writing deployment files.
49
+ def audit
50
+ @unsupported.clear
51
+ (@source.units_of('Forms$Layout') + @source.units_of('Forms$Page')).each do |unit|
52
+ name = "#{unit.module_name}.#{unit.document['Name']}"
53
+ audit_unsupported_widgets(unit.document, name)
54
+ descendants(unit.document).select { _1['$Type'] == 'Forms$DataGrid' }.each_with_index do |grid, index|
55
+ compiler = LegacyDataGridCompiler.new(@source, name, grid, language: 'en_US', sequence: index + 1)
56
+ compiler.html
57
+ @unsupported[name].concat(compiler.unsupported)
58
+ end
59
+ end
60
+ frozen_unsupported
61
+ end
62
+
63
+ private
64
+
65
+ def write_page(unit, language)
66
+ name = "#{unit.module_name}.#{unit.document['Name']}"
67
+ audit_unsupported_widgets(unit.document, name)
68
+ @widget_sequence = 0
69
+ rendered = page_widgets(unit.document).map { render_widget(_1, name, language) }.join
70
+ path = File.join(@web_root, 'pages', language, unit.module_name, "#{unit.document['Name']}.page.xml")
71
+ FileUtils.mkdir_p(File.dirname(path))
72
+ File.binwrite(path, page_xml(unit, language, rendered))
73
+ end
74
+
75
+ def write_layout(unit, language)
76
+ name = "#{unit.module_name}.#{unit.document['Name']}"
77
+ audit_unsupported_widgets(unit.document, name)
78
+ @widget_sequence = 0
79
+ rendered = layout_widgets(unit.document).map { render_widget(_1, name, language) }.join
80
+ path = File.join(@web_root, 'pages', language, unit.module_name, "#{unit.document['Name']}.layout.xml")
81
+ FileUtils.mkdir_p(File.dirname(path))
82
+ File.binwrite(path, layout_xml(unit, rendered))
83
+ end
84
+
85
+ def audit_unsupported_widgets(root, page_name)
86
+ visit = lambda do |value|
87
+ case value
88
+ when Hash
89
+ type = value['$Type'].to_s
90
+ if type == 'Forms$DataGrid'
91
+ return
92
+ elsif VISUAL_WIDGET_TYPES.include?(type) && !supported_widget_type?(type)
93
+ @unsupported[page_name] << type
94
+ elsif type == 'Forms$DynamicText' && !array(value.dig('Content', 'Parameters')).empty?
95
+ @unsupported[page_name] << 'Forms$DynamicText(parameters)'
96
+ elsif type == 'Forms$DivContainer'
97
+ action_type = value.dig('OnClickAction', '$Type').to_s
98
+ @unsupported[page_name] << 'Forms$DivContainer(onClick)' \
99
+ unless action_type.empty? || action_type == 'Forms$NoAction'
100
+ end
101
+
102
+ value.each_value { visit.call(_1) }
103
+ when Array then value.each { visit.call(_1) }
104
+ end
105
+ end
106
+ visit.call(root)
107
+ end
108
+
109
+ def page_xml(unit, language, content)
110
+ page = unit.document
111
+ title = translated(page['Title'], language)
112
+ layout = page.dig('FormCall', 'Form').to_s.tr('.', '/')
113
+ argument = array(page.dig('FormCall', 'Arguments')).first
114
+ parameter = layout_argument_id(argument&.fetch('Parameter', '').to_s)
115
+ css = page_css_classes(page)
116
+ class_attribute = css.empty? ? '' : " class='#{escape(css)}'"
117
+ "\uFEFF<?xml version='1.0' encoding='utf-8'?>" \
118
+ "<m:page id='#{unit.id}' xmlns='http://www.w3.org/1999/xhtml' " \
119
+ "title='#{escape(title)}'#{class_attribute} xmlns:m='http://schemas.mendix.com/forms/1.0'>" \
120
+ "<m:layouts>#{layout.empty? ? '' : "<m:layout path='#{escape(layout)}.layout.xml'></m:layout>"}</m:layouts>" \
121
+ "<m:arguments><m:argument parameterName='#{escape(parameter)}'>#{content}</m:argument></m:arguments>" \
122
+ '</m:page>'
123
+ end
124
+
125
+ def layout_xml(unit, content)
126
+ "\uFEFF<?xml version='1.0' encoding='utf-8'?>" \
127
+ "<m:layout id='#{unit.id}' xmlns='http://www.w3.org/1999/xhtml' " \
128
+ "xmlns:m='http://schemas.mendix.com/forms/1.0'>" \
129
+ "<m:arguments><m:argument>#{content}</m:argument></m:arguments></m:layout>"
130
+ end
131
+
132
+ def render_widget(widget, page_name, language)
133
+ return '' unless widget.is_a?(Hash)
134
+
135
+ case widget['$Type']
136
+ when 'Forms$VerticalFlow'
137
+ widget_children(widget).map { render_widget(_1, page_name, language) }.join
138
+ when 'Forms$Placeholder'
139
+ identifier = IO::BsonCodec.extract_id(widget['$ID']) || widget['Name'].to_s
140
+ "<div data-mx-placeholder='#{escape(identifier)}' class='mx-placeholder'></div>"
141
+ when 'Forms$DivContainer'
142
+ render_container(widget, page_name, language)
143
+ when 'Forms$DynamicText'
144
+ render_dynamic_text(widget, language)
145
+ when 'Forms$DataGrid'
146
+ render_data_grid(widget, page_name, language)
147
+ else
148
+ ''
149
+ end
150
+ end
151
+
152
+ def render_container(widget, page_name, language)
153
+ children = widget_children(widget).map { render_widget(_1, page_name, language) }.join
154
+ tag = widget['RenderMode'].to_s == 'Span' ? 'span' : 'div'
155
+ "<#{tag}#{html_attributes(widget, base: nil)}>#{children}</#{tag}>"
156
+ end
157
+
158
+ def render_dynamic_text(widget, language)
159
+ content = widget['Content'] || {}
160
+ text = translated(content['Template'], language)
161
+ text = translated(content['Fallback'], language) if text.empty?
162
+ tag = widget['RenderMode'].to_s == 'Paragraph' ? 'p' : 'span'
163
+ attributes = html_attributes(widget, base: 'mx-text', mendix_id: next_widget_id)
164
+ "<#{tag}#{attributes}>#{escape(text)}</#{tag}>"
165
+ end
166
+
167
+ def render_data_grid(widget, page_name, language)
168
+ compiler = LegacyDataGridCompiler.new(
169
+ @source, page_name, widget, language:, sequence: next_widget_sequence
170
+ )
171
+ compiler.html.tap { @unsupported[page_name].concat(compiler.unsupported) }
172
+ end
173
+
174
+ def html_attributes(widget, base:, mendix_id: nil)
175
+ values = {}
176
+ css = css_classes(widget, base:)
177
+ values['class'] = css unless css.empty?
178
+ style = widget['Style'].to_s
179
+ style = widget.dig('Appearance', 'Style').to_s if style.empty?
180
+ values['style'] = style unless style.empty?
181
+ values['data-mendix-id'] = mendix_id if mendix_id
182
+ values.map { |key, value| " #{key}='#{escape(value)}'" }.join
183
+ end
184
+
185
+ def css_classes(widget, base:)
186
+ name = widget['Name'].to_s
187
+ classes = [base, ("mx-name-#{name}" unless name.empty?), widget['Class'],
188
+ widget.dig('Appearance', 'Class')]
189
+ classes.compact.flat_map { _1.to_s.split }.reject(&:empty?).uniq.join(' ')
190
+ end
191
+
192
+ def page_css_classes(page)
193
+ layout = layout_unit(page.dig('FormCall', 'Form').to_s)&.document || {}
194
+ [layout['Class'], layout.dig('Appearance', 'Class'), page['Class'],
195
+ page.dig('Appearance', 'Class')].compact.flat_map { _1.to_s.split }
196
+ .reject(&:empty?).uniq.join(' ')
197
+ end
198
+
199
+ def page_widgets(document)
200
+ argument = array(document.dig('FormCall', 'Arguments')).first
201
+ widget_children(argument || document)
202
+ end
203
+
204
+ def layout_widgets(document) = widget_children(document)
205
+
206
+ def widget_children(value)
207
+ return [] unless value.is_a?(Hash)
208
+
209
+ plural = array(value['Widgets'])
210
+ singular = value['Widget']
211
+ plural.empty? && singular.is_a?(Hash) ? [singular] : plural
212
+ end
213
+
214
+ def supported_widget_type?(type)
215
+ %w[Forms$DataGrid Forms$DivContainer Forms$DynamicText].include?(type)
216
+ end
217
+
218
+ def layout_argument_id(parameter)
219
+ module_name, layout_name, placeholder_name = parameter.split('.', 3)
220
+ return parameter unless placeholder_name
221
+
222
+ layout = layout_unit("#{module_name}.#{layout_name}")
223
+ placeholder = descendants(layout&.document || {}).find do |value|
224
+ value['$Type'] == 'Forms$Placeholder' && value['Name'] == placeholder_name
225
+ end
226
+ IO::BsonCodec.extract_id(placeholder&.fetch('$ID', nil)) || parameter
227
+ end
228
+
229
+ def layout_unit(qualified_name)
230
+ module_name, layout_name = qualified_name.split('.', 2)
231
+ @source.units_of('Forms$Layout').find do |unit|
232
+ unit.module_name == module_name && unit.document['Name'] == layout_name
233
+ end
234
+ end
235
+
236
+ def next_widget_sequence
237
+ @widget_sequence += 1
238
+ end
239
+
240
+ def next_widget_id = "1_#{next_widget_sequence - 1}"
241
+
242
+ def descendants(root)
243
+ result = []
244
+ visit = lambda do |value|
245
+ case value
246
+ when Hash
247
+ result << value if value['$Type']
248
+ value.each_value { visit.call(_1) }
249
+ when Array then value.each { visit.call(_1) }
250
+ end
251
+ end
252
+ visit.call(root)
253
+ result
254
+ end
255
+
256
+ def languages
257
+ found = @source.documents.flat_map do |document|
258
+ descendants(document).filter_map do |value|
259
+ value['LanguageCode'].to_s if value['$Type'] == 'Texts$Translation'
260
+ end
261
+ end.reject(&:empty?).uniq.sort
262
+ found.empty? ? ['en_US'] : found
263
+ end
264
+
265
+ def translated(text, language)
266
+ items = array(text&.fetch('Items', nil))
267
+ items.find { _1['LanguageCode'] == language }&.fetch('Text', '') ||
268
+ items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') || ''
269
+ end
270
+
271
+ def write_manifest
272
+ manifest = { 'profiles' => @profiles.map(&:to_s), 'unsupportedWidgets' => frozen_unsupported }
273
+ File.write(File.join(@web_root, 'mxrb-legacy-pages.json'), JSON.pretty_generate(manifest))
274
+ end
275
+
276
+ def frozen_unsupported
277
+ @unsupported.transform_values { _1.reject(&:empty?).uniq.sort }.freeze
278
+ end
279
+
280
+ def escape(value)
281
+ value.to_s.gsub('&', '&amp;').gsub("'", '&#39;').gsub('<', '&lt;').gsub('>', '&gt;')
282
+ end
283
+ end
284
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
285
+ # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
286
+ end
287
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+ require 'zip'
6
+
7
+ module Mxrb
8
+ module Compiler
9
+ # Safe inspection and content-level comparison of Mendix deployment archives.
10
+ module Mda
11
+ Entry = Data.define(:path, :size, :crc, :sha256, :directory)
12
+ Inspection = Data.define(:path, :metadata, :entries, :sha256) do
13
+ def files = entries.reject(&:directory)
14
+ def roots = entries.map { _1.path.split('/').first }.uniq.sort
15
+ end
16
+ Difference = Data.define(:path, :status, :left_sha256, :right_sha256)
17
+
18
+ module_function
19
+
20
+ def inspect(path)
21
+ archive_path = File.expand_path(path)
22
+ entries = read_entries(archive_path)
23
+ metadata_entry = entries.find { _1.path == 'model/metadata.json' }
24
+ metadata = read_metadata(archive_path, metadata_entry)
25
+ raise CompilationError, 'MDA has no model/metadata.json' unless metadata
26
+
27
+ build_inspection(archive_path, metadata, entries)
28
+ rescue Zip::Error, JSON::ParserError => e
29
+ raise CompilationError, "invalid MDA #{archive_path}: #{e.message}"
30
+ end
31
+
32
+ def compare(left, right)
33
+ left_entries = entry_map(left)
34
+ right_entries = entry_map(right)
35
+ (left_entries.keys | right_entries.keys).sort.filter_map do |path|
36
+ lhs = left_entries[path]
37
+ rhs = right_entries[path]
38
+ status = difference_status(lhs, rhs)
39
+ next unless status
40
+
41
+ Difference.new(path:, status:, left_sha256: lhs&.sha256, right_sha256: rhs&.sha256)
42
+ end
43
+ end
44
+
45
+ def read_entries(archive_path)
46
+ Zip::File.open(archive_path) do |archive|
47
+ archive.map { build_entry(_1) }
48
+ end
49
+ end
50
+
51
+ def build_inspection(path, metadata, entries)
52
+ Inspection.new(
53
+ path:, metadata:, entries: entries.sort_by(&:path),
54
+ sha256: Digest::SHA256.file(path).hexdigest
55
+ )
56
+ end
57
+
58
+ def build_entry(entry)
59
+ path = safe_path(entry.name)
60
+ directory = entry.directory?
61
+ bytes = directory ? '' : entry.get_input_stream.read
62
+ Entry.new(
63
+ path:, size: entry.size, crc: entry.crc,
64
+ sha256: directory ? nil : Digest::SHA256.hexdigest(bytes), directory:
65
+ )
66
+ end
67
+
68
+ def read_metadata(archive_path, entry)
69
+ return unless entry
70
+
71
+ Zip::File.open(archive_path) do |archive|
72
+ JSON.parse(archive.get_entry(entry.path).get_input_stream.read)
73
+ end
74
+ end
75
+
76
+ def entry_map(path)
77
+ inspect(path).files.to_h { [_1.path, _1] }
78
+ end
79
+
80
+ def difference_status(left, right)
81
+ return :added unless left
82
+ return :removed unless right
83
+ return :changed if left.sha256 != right.sha256
84
+
85
+ nil
86
+ end
87
+
88
+ def safe_path(path)
89
+ clean = path.to_s.tr('\\', '/').sub(%r{\A/+}, '')
90
+ parts = clean.split('/')
91
+ if clean.empty? || parts.include?('..') || parts.include?('.')
92
+ raise CompilationError, "unsafe MDA entry #{path.inspect}"
93
+ end
94
+
95
+ clean
96
+ end
97
+ end
98
+ end
99
+ end