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,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Mxrb
6
+ module Compiler
7
+ # Compiles the official React Gallery widget with XPath, microflow, or nanoflow data sources.
8
+ class GalleryBundleCompiler # rubocop:disable Metrics/ClassLength
9
+ include ModelValues
10
+
11
+ WIDGET_ID = 'com.mendix.widget.web.gallery.Gallery'
12
+
13
+ attr_reader :data_source, :entity_name
14
+
15
+ def initialize(source, page_name, widget)
16
+ @source = source
17
+ @page_name = page_name
18
+ @widget = widget
19
+ @index = document_index
20
+ @values = property_values(widget['Object'])
21
+ @data_source = WebListDataSource.new(source, widget)
22
+ @entity_name = data_source.entity
23
+ @xpath_arguments = resolve_xpath_arguments
24
+ end
25
+
26
+ def supported?
27
+ widget_type&.fetch('WidgetId', nil) == WIDGET_ID &&
28
+ @data_source.supported? && !entity_name.to_s.empty? && !@xpath_arguments.nil?
29
+ end
30
+
31
+ def content_widgets = array(@values['content']&.last&.fetch('Widgets', nil))
32
+ def widget_key = "p.#{@page_name}.#{@widget['Name']}"
33
+ def data_source_id = "p.#{Digest::SHA256.hexdigest(widget_key)[0, 6].to_i(16)}"
34
+
35
+ def render(content, nanoflow_reference: nil)
36
+ @nanoflow_reference = nanoflow_reference
37
+ values = primitives(@values).merge(
38
+ key: widget_key, '$widgetId': widget_key, datasource: raw(datasource),
39
+ content: raw("TemplatedWidgetProperty({ children: () => #{content}, " \
40
+ "dataSourceId: #{JSON.generate(data_source_id)}, editable: false })"),
41
+ itemSelection: raw(selection), class: css_class
42
+ )
43
+ "React.createElement($Gallery, #{js_object(values)})"
44
+ end
45
+
46
+ private
47
+
48
+ def datasource
49
+ config = {
50
+ dataSourceId: data_source_id,
51
+ operationId: WebOperationCompiler.operation_id(@page_name, @widget['Name'])
52
+ }
53
+ return xpath_datasource(config) if @data_source.xpath?
54
+ return microflow_datasource(config) if @data_source.microflow?
55
+
56
+ nanoflow_datasource(config)
57
+ end
58
+
59
+ def xpath_datasource(config)
60
+ values = config.merge(entity: entity_name, sort: [])
61
+ unless @xpath_arguments.empty?
62
+ values[:arguments] = @xpath_arguments
63
+ values[:fetchOnlyWithAllParams] = true
64
+ end
65
+ "DatabaseObjectListProperty(#{js_object(values)})"
66
+ end
67
+
68
+ def resolve_xpath_arguments
69
+ return {} unless @data_source.xpath?
70
+
71
+ names = xpath_variables(@data_source.xpath_constraint)
72
+ return {} if names.empty?
73
+
74
+ parameters = object_page_parameters(page_document, names)
75
+ return unless parameters
76
+
77
+ parameters.to_h { |name, _entity| [name, ["$#{name}", :undefined, false]] }
78
+ end
79
+
80
+ def page_document
81
+ module_name, document_name = @page_name.split('.', 2)
82
+ @source.units_of('Forms$Page').find do |unit|
83
+ unit.module_name == module_name && unit.document['Name'] == document_name
84
+ end&.document
85
+ end
86
+
87
+ def microflow_datasource(config)
88
+ values = config.merge(argMap: {}, fetchOnlyWithAllParams: false)
89
+ "MicroflowObjectListProperty(#{js_object(values)})"
90
+ end
91
+
92
+ def nanoflow_datasource(config)
93
+ values = config.except(:operationId).merge(
94
+ source: { nanoflow: raw(@nanoflow_reference || 'undefined') },
95
+ argMap: {}, fetchOnlyWithAllParams: false
96
+ )
97
+ "NanoflowObjectListProperty(#{js_object(values)})"
98
+ end
99
+
100
+ def selection
101
+ type = @values['itemSelection']&.last&.fetch('Selection', 'None') || 'None'
102
+ "SelectionProperty(#{js_object(selectionType: type, dataSourceId: data_source_id)})"
103
+ end
104
+
105
+ def primitives(values)
106
+ values.each_with_object({}) do |(key, pair), result|
107
+ type, value = pair
108
+ compiled = primitive(type, value)
109
+ result[key.to_sym] = compiled unless compiled.equal?(:undefined)
110
+ end
111
+ end
112
+
113
+ def primitive(type, value) # rubocop:disable Metrics/CyclomaticComplexity
114
+ compiled = case type
115
+ when 'Boolean' then value['PrimitiveValue'] == 'true'
116
+ when 'Integer' then value['PrimitiveValue'].to_i
117
+ when 'Enumeration' then value['PrimitiveValue'].to_s
118
+ when 'TextTemplate' then raw(expression(translated_text(value['TextTemplate'])))
119
+ when 'Widgets' then [] if array(value['Widgets']).empty?
120
+ end
121
+ compiled.nil? ? :undefined : compiled
122
+ end
123
+
124
+ def expression(value)
125
+ "ExpressionProperty(#{js_object(expression: { expr: { type: 'literal', value: }, args: {} })})"
126
+ end
127
+
128
+ def raw(value) = { '$raw' => value }
129
+
130
+ def js_object(value)
131
+ pairs = value.map { |key, item| "#{JSON.generate(key)}: #{js_value(item)}" }
132
+ "{ #{pairs.join(', ')} }"
133
+ end
134
+
135
+ def js_value(value)
136
+ return value['$raw'] if value.is_a?(Hash) && value.key?('$raw')
137
+ return 'undefined' if value == :undefined
138
+ return "[#{value.map { js_value(_1) }.join(', ')}]" if value.is_a?(Array)
139
+ return js_object(value) if value.is_a?(Hash)
140
+
141
+ JSON.generate(value)
142
+ end
143
+
144
+ def property_values(object)
145
+ array(object&.fetch('Properties', nil)).filter_map do |property|
146
+ type = @index[IO::BsonCodec.extract_id(property['TypePointer'])]
147
+ next unless type
148
+
149
+ [type.fetch('PropertyKey'), [type.dig('ValueType', 'Type'), property['Value']]]
150
+ end.to_h
151
+ end
152
+
153
+ def css_class
154
+ ["mx-name-#{@widget['Name']}", @widget.dig('Appearance', 'Class')]
155
+ .map(&:to_s).reject(&:empty?).uniq.join(' ')
156
+ end
157
+
158
+ def translated_text(text)
159
+ items = array(text&.dig('Template', 'Items'))
160
+ items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') ||
161
+ items.first&.fetch('Text', '') || ''
162
+ end
163
+
164
+ def widget_type
165
+ object_type_id = IO::BsonCodec.extract_id(@widget.dig('Object', 'TypePointer'))
166
+ return unless object_type_id
167
+
168
+ @index.values.find do |document|
169
+ IO::BsonCodec.extract_id(document.dig('ObjectType', '$ID')) == object_type_id
170
+ end
171
+ end
172
+
173
+ def document_index
174
+ return @source.document_index if @source.is_a?(SourceModel)
175
+
176
+ {}.tap { |index| @source.documents.each { index_document(_1, index) } }
177
+ end
178
+
179
+ def index_document(value, index)
180
+ case value
181
+ when Hash
182
+ id = IO::BsonCodec.extract_id(value['$ID'])
183
+ index[id] = value if id
184
+ value.each_value { index_document(_1, index) }
185
+ when Array then value.each { index_document(_1, index) }
186
+ end
187
+ end
188
+ end # rubocop:enable Metrics/ClassLength
189
+ end
190
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'zip'
5
+
6
+ module Mxrb
7
+ module Compiler
8
+ # Compiles schema-described pluggable widgets whose properties use the standard client contracts.
9
+ # rubocop:disable Metrics
10
+ class GenericWidgetBundleCompiler
11
+ include ModelValues
12
+
13
+ attr_reader :component_name, :module_path
14
+
15
+ def initialize(source, page_name, widget, scope:, entity:, render_widgets: nil, key_prefix: 'p',
16
+ action_property: nil)
17
+ @source = source
18
+ @page_name = page_name
19
+ @widget = widget
20
+ @scope = scope
21
+ @entity = entity.to_s
22
+ @render_widgets = render_widgets
23
+ @key_prefix = key_prefix
24
+ @action_property = action_property
25
+ @index = source.document_index
26
+ @widget_type = find_widget_type
27
+ @values = property_values(widget['Object'])
28
+ @list_data_source = @values.values.find { _1.first == 'DataSource' }&.last
29
+ @component_name = widget_id.to_s.split('.').last
30
+ @module_path = widget_id.to_s.tr('.', '/')
31
+ end
32
+
33
+ def supported?
34
+ present_identifier?(@component_name) && package_module? &&
35
+ @values.values.all? { supported_property?(_1) }
36
+ end
37
+
38
+ def render
39
+ properties = @values.each_with_object({}) do |(key, pair), result|
40
+ compiled = compile_property(pair)
41
+ result[key.to_sym] = compiled unless compiled.equal?(:undefined)
42
+ end
43
+ properties.merge!(key: widget_key, '$widgetId': widget_key, class: css_class)
44
+ "React.createElement($#{@component_name}, #{javascript(properties)})"
45
+ end
46
+
47
+ private
48
+
49
+ def widget_id = @widget_type&.fetch('WidgetId', nil)
50
+
51
+ def find_widget_type
52
+ object_type_id = IO::BsonCodec.extract_id(@widget.dig('Object', 'TypePointer'))
53
+ @index.values.find do |document|
54
+ IO::BsonCodec.extract_id(document.dig('ObjectType', '$ID')) == object_type_id
55
+ end
56
+ end
57
+
58
+ def property_values(object)
59
+ array(object&.fetch('Properties', nil)).filter_map do |property|
60
+ type = @index[IO::BsonCodec.extract_id(property['TypePointer'])]
61
+ next unless type
62
+
63
+ [type.fetch('PropertyKey'), [type.dig('ValueType', 'Type'), property['Value']]]
64
+ end.to_h
65
+ end
66
+
67
+ def supported_property?(pair)
68
+ type, value = pair
69
+ return true if %w[Boolean Integer Decimal Enumeration String System].include?(type)
70
+ return value['DataSource'].nil? || !database_list_property(value).nil? if type == 'DataSource'
71
+ return true if type == 'Association'
72
+ return true if type == 'Attribute' && value['AttributeRef'].nil?
73
+ return true if %w[Expression TextTemplate].include?(type) && expression_property(type, value)
74
+ return true if type == 'Attribute' && attribute_property(value)
75
+ return no_action?(value) || !compiled_action(value).nil? if type == 'Action'
76
+ return array(value['Widgets']).empty? || @render_widgets if type == 'Widgets'
77
+ return array(value['Objects']).empty? if type == 'Object'
78
+
79
+ false
80
+ end
81
+
82
+ def compile_property(pair)
83
+ type, value = pair
84
+ case type
85
+ when 'Boolean' then value['PrimitiveValue'] == 'true'
86
+ when 'Integer' then value['PrimitiveValue'].to_i
87
+ when 'Decimal' then value['PrimitiveValue'].to_f
88
+ when 'Enumeration', 'String' then value['PrimitiveValue'].to_s
89
+ when 'Expression', 'TextTemplate' then raw(expression_property(type, value))
90
+ when 'DataSource' then database_list_property(value)&.then { raw(_1) } || :undefined
91
+ when 'Action' then compiled_action(value)&.then { raw(_1) } || :undefined
92
+ when 'Association' then :undefined
93
+ when 'Attribute' then attribute_property(value)&.then { raw(_1) } || :undefined
94
+ when 'Widgets' then compile_widgets(value)
95
+ when 'Object' then []
96
+ else :undefined
97
+ end
98
+ end
99
+
100
+ def compile_widgets(value)
101
+ widgets = array(value['Widgets'])
102
+ return [] if widgets.empty?
103
+
104
+ @render_widgets.call(widgets)
105
+ end
106
+
107
+ def expression_property(type, value)
108
+ expression = type == 'Expression' ? compile_expression(value['Expression']) : compile_template(value)
109
+ return unless expression
110
+
111
+ args = expression_variables(expression).to_h do |variable|
112
+ [variable.to_sym, { widget: @scope, source: 'object' }]
113
+ end
114
+ property = @list_data_source ? 'ListExpressionProperty' : 'ExpressionProperty'
115
+ config = { expression: { expr: expression, args: } }
116
+ config[:dataSourceId] = widget_key if @list_data_source
117
+ "#{property}(#{javascript(config)})"
118
+ end
119
+
120
+ def database_list_property(value)
121
+ source = value['DataSource'] || {}
122
+ return unless source['$Type'] == 'CustomWidgets$CustomWidgetXPathSource'
123
+
124
+ entity = source.dig('EntityRef', 'Entity').to_s
125
+ return unless present_qualified_name?(entity)
126
+
127
+ config = {
128
+ dataSourceId: widget_key, entity:,
129
+ operationId: WebOperationCompiler.operation_id(@page_name, @widget['Name']),
130
+ sort: sort_items(source)
131
+ }
132
+ "DatabaseObjectListProperty(#{javascript(config)})"
133
+ end
134
+
135
+ def compiled_action(value)
136
+ action = value['Action'] || {}
137
+ return if action['$Type'].to_s.empty? || action['$Type'] == 'Forms$NoAction'
138
+
139
+ @action_property&.call(action)
140
+ end
141
+
142
+ def sort_items(source)
143
+ array(source.dig('SortBar', 'SortItems')).filter_map do |item|
144
+ attribute = item.dig('AttributeRef', 'Attribute').to_s.split('.').last
145
+ next unless present_identifier?(attribute)
146
+
147
+ [attribute, item['SortOrder'].to_s.casecmp('descending').zero? ? 'desc' : 'asc']
148
+ end
149
+ end
150
+
151
+ def compile_expression(source)
152
+ value = source.to_s.strip
153
+ return { type: 'literal', value: nil } if value.empty? || value == 'empty'
154
+ return variable_expression(value) if value.match?(%r{\A\$[A-Za-z_]\w*(?:/[A-Za-z_]\w*)*\z})
155
+ return { type: 'literalNumeric', value: } if value.match?(/\A-?\d+(?:\.\d+)?\z/)
156
+ return { type: 'literal', value: value == 'true' } if %w[true false].include?(value)
157
+ return { type: 'literal', value: value[1..-2] } if quoted?(value)
158
+
159
+ nil
160
+ end
161
+
162
+ def variable_expression(source)
163
+ _variable, *path = source.delete_prefix('$').split('/')
164
+ { type: 'variable', variable: 'currentObject' }.tap do |result|
165
+ result[:path] = path.join('/') unless path.empty?
166
+ end
167
+ end
168
+
169
+ def compile_template(value)
170
+ template = value['TextTemplate']
171
+ text = translated_text(template)
172
+ parameters = array(template&.fetch('Parameters', nil))
173
+ return { type: 'literal', value: text } if parameters.empty?
174
+
175
+ expressions = parameters.map { template_parameter(_1) }
176
+ return unless expressions.all?
177
+ return expressions.first if text == '{1}' && expressions.one?
178
+
179
+ format_expression(text, expressions)
180
+ end
181
+
182
+ def template_parameter(parameter)
183
+ attribute = parameter.dig('AttributeRef', 'Attribute').to_s
184
+ return compile_expression(parameter['Expression']) if attribute.empty?
185
+
186
+ name = attribute.split('.').last
187
+ variable = { type: 'variable', variable: 'currentObject', path: name }
188
+ enumeration = enumeration_name(attribute)
189
+ return variable unless enumeration
190
+
191
+ { type: 'function', name: 'getCaption',
192
+ parameters: [variable, { type: 'literal', value: enumeration }] }
193
+ end
194
+
195
+ def format_expression(text, expressions)
196
+ pieces = text.split(/(\{\d+\})/).filter_map do |piece|
197
+ index = piece[/\A\{(\d+)\}\z/, 1]
198
+ index ? expressions[index.to_i - 1] : ({ type: 'literal', value: piece } unless piece.empty?)
199
+ end
200
+ pieces.compact.reduce { |left, right| { type: 'function', name: '+', parameters: [left, right] } }
201
+ end
202
+
203
+ def attribute_property(value)
204
+ attribute = value.dig('AttributeRef', 'Attribute').to_s
205
+ return unless @scope && attribute.include?('.')
206
+
207
+ entity, _, name = attribute.rpartition('.')
208
+ "AttributeProperty(#{javascript(
209
+ scope: @scope, path: '', entity:, attribute: name,
210
+ onChange: { type: 'doNothing', argMap: {}, config: {}, disabledDuringExecution: true },
211
+ isList: false, validation: nil, formatting: {}
212
+ )})"
213
+ end
214
+
215
+ def expression_variables(expression)
216
+ case expression
217
+ when Hash
218
+ own = expression[:type] == 'variable' ? [expression[:variable]] : []
219
+ own + expression.values.flat_map { expression_variables(_1) }
220
+ when Array then expression.flat_map { expression_variables(_1) }
221
+ else []
222
+ end.uniq
223
+ end
224
+
225
+ def enumeration_name(attribute)
226
+ module_name, entity_name, attribute_name = attribute.split('.', 3)
227
+ domain = @source.units_of('DomainModels$DomainModel').find { _1.module_name == module_name }
228
+ entity = array(domain&.document&.fetch('Entities', nil)).find { _1['Name'] == entity_name }
229
+ member = array(entity&.fetch('Attributes', nil)).find { _1['Name'] == attribute_name }
230
+ member&.dig('NewType', 'Enumeration').to_s.then { _1.empty? ? nil : _1 }
231
+ end
232
+
233
+ def translated_text(template)
234
+ items = array(template&.dig('Template', 'Items'))
235
+ items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') ||
236
+ items.first&.fetch('Text', '') || ''
237
+ end
238
+
239
+ def package_module?
240
+ root = File.dirname(@source.path)
241
+ direct = File.join(root, 'widgets', "#{@module_path}.mjs")
242
+ return true if File.file?(direct)
243
+
244
+ entry = "#{@module_path}.mjs"
245
+ Dir.glob(File.join(root, 'widgets', '*.mpk')).any? do |path|
246
+ Zip::File.open(path) { _1.find_entry(entry) }
247
+ rescue Zip::Error
248
+ false
249
+ end
250
+ end
251
+
252
+ def no_action?(value) = value['Action'].nil? || value.dig('Action', '$Type') == 'Forms$NoAction'
253
+ def widget_key = "#{@key_prefix}.#{@page_name}.#{@widget['Name']}"
254
+
255
+ def css_class
256
+ ["mx-name-#{@widget['Name']}", @widget.dig('Appearance', 'Class')]
257
+ .map(&:to_s).reject(&:empty?).uniq.join(' ')
258
+ end
259
+
260
+ def present_identifier?(value) = value.to_s.match?(/\A[A-Za-z_$][A-Za-z0-9_$]*\z/)
261
+
262
+ def present_qualified_name?(value)
263
+ value.to_s.split('.').length >= 2 && value.to_s.split('.').all? { present_identifier?(_1) }
264
+ end
265
+
266
+ def quoted?(value) = value.start_with?("'", '"') && value.end_with?(value[0])
267
+ def raw(value) = { '$raw' => value }
268
+
269
+ def javascript(value)
270
+ return value['$raw'] if value.is_a?(Hash) && value.key?('$raw')
271
+ return "[#{value.map { javascript(_1) }.join(', ')}]" if value.is_a?(Array)
272
+ if value.is_a?(Hash)
273
+ return "{ #{value.map { |key, item| "#{JSON.generate(key)}: #{javascript(item)}" }.join(', ')} }"
274
+ end
275
+
276
+ JSON.generate(value)
277
+ end
278
+ end
279
+ # rubocop:enable Metrics
280
+ end
281
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Mxrb
6
+ module Compiler
7
+ # Compiles the static-image subset of the official React Image widget.
8
+ # rubocop:disable Metrics
9
+ class ImageBundleCompiler
10
+ include ModelValues
11
+
12
+ WIDGET_ID = 'com.mendix.widget.web.image.Image'
13
+
14
+ def self.render_static(key, css_class, uri, options) # rubocop:disable Metrics/MethodLength
15
+ properties = {
16
+ key:, '$widgetId': key, datasource: 'image',
17
+ imageObject: raw("WebStaticImageProperty({ image: { uri: #{JSON.generate(uri)} } })"),
18
+ imageUrl: raw(expression('')), isBackgroundImage: false, onClickType: 'action',
19
+ alternativeText: raw(expression('')), widthUnit: unit(options[:width_unit]),
20
+ width: number(options[:width], 100), heightUnit: unit(options[:height_unit]),
21
+ height: number(options[:height], 100), iconSize: 14,
22
+ displayAs: 'fullImage', responsive: options[:responsive], minHeightUnit: 'none', minHeight: 0,
23
+ maxHeightUnit: 'none', maxHeight: 0, class: css_class
24
+ }
25
+ "React.createElement($Image, #{javascript(properties)})"
26
+ end # rubocop:enable Metrics/MethodLength
27
+
28
+ def self.expression(value)
29
+ "ExpressionProperty({ expression: { expr: { type: \"literal\", value: #{JSON.generate(value)} }, args: {} } })"
30
+ end
31
+
32
+ def self.raw(value) = { '$raw' => value }
33
+ def self.unit(value) = value.to_s.downcase.then { _1.empty? ? 'auto' : _1 }
34
+ def self.number(value, fallback) = Integer(value || fallback)
35
+
36
+ def self.javascript(value)
37
+ return value['$raw'] if value.is_a?(Hash) && value.key?('$raw')
38
+ return "[#{value.map { javascript(_1) }.join(', ')}]" if value.is_a?(Array)
39
+ if value.is_a?(Hash)
40
+ return "{ #{value.map { |key, item| "#{JSON.generate(key)}: #{javascript(item)}" }.join(', ')} }"
41
+ end
42
+
43
+ JSON.generate(value)
44
+ end
45
+
46
+ def initialize(source, page_name, widget, scope: nil, entity: nil, key_prefix: 'p', action_property: nil)
47
+ @source = source
48
+ @page_name = page_name
49
+ @widget = widget
50
+ @scope = scope
51
+ @entity = entity
52
+ @key_prefix = key_prefix
53
+ @action_property = action_property
54
+ @index = document_index
55
+ @values = property_values(widget['Object'])
56
+ end
57
+
58
+ def supported?
59
+ type = widget_type
60
+ type && type.fetch('WidgetId', nil) == WIDGET_ID && supported_source? && supported_action?
61
+ end
62
+
63
+ def render # rubocop:disable Metrics/AbcSize
64
+ values = primitive_properties.merge(
65
+ key: widget_key, '$widgetId': widget_key,
66
+ alternativeText: self.class.raw(self.class.expression(text_value('alternativeText'))),
67
+ class: css_class
68
+ )
69
+ if primitive('datasource') == 'image'
70
+ values[:imageObject] = self.class.raw(
71
+ "WebStaticImageProperty({ image: { uri: #{JSON.generate(image_uri)} } })"
72
+ )
73
+ values[:imageUrl] = self.class.raw(self.class.expression(text_value('imageUrl')))
74
+ else
75
+ values[:imageUrl] = self.class.raw(dynamic_image_url)
76
+ end
77
+ action = compiled_action
78
+ values[:onClick] = self.class.raw(action) if action
79
+ "React.createElement($Image, #{self.class.javascript(values)})"
80
+ end # rubocop:enable Metrics/AbcSize
81
+
82
+ private
83
+
84
+ def supported_source?
85
+ return !image_uri.nil? if primitive('datasource') == 'image'
86
+
87
+ primitive('datasource') == 'imageUrl' && !dynamic_image_url.nil?
88
+ end
89
+
90
+ def supported_action?
91
+ pair = @values['onClick']
92
+ action = pair&.last&.fetch('Action', nil)
93
+ action.nil? || action['$Type'] == 'Forms$NoAction' || !compiled_action.nil?
94
+ end
95
+
96
+ def compiled_action
97
+ pair = @values['onClick']
98
+ action = pair&.last&.fetch('Action', nil)
99
+ return if action.nil? || action['$Type'] == 'Forms$NoAction'
100
+
101
+ @action_property&.call(action)
102
+ end
103
+
104
+ def dynamic_image_url
105
+ parameter = image_url_parameter
106
+ attribute = parameter&.dig('AttributeRef', 'Attribute').to_s
107
+ return unless @scope && attribute.include?('.')
108
+
109
+ path = array(parameter.dig('AttributeRef', 'EntityRef', 'Steps')).flat_map do |step|
110
+ [step['Association'], step['DestinationEntity']]
111
+ end.map(&:to_s).reject(&:empty?).join('/')
112
+ member = [path, attribute.split('.').last].reject(&:empty?).join('/')
113
+ config = {
114
+ expression: {
115
+ expr: { type: 'variable', variable: 'currentObject', path: member },
116
+ args: { currentObject: { widget: @scope, source: 'object' } }
117
+ }
118
+ }
119
+ "ExpressionProperty(#{self.class.javascript(config)})"
120
+ end
121
+
122
+ def image_url_parameter
123
+ template = @values['imageUrl']&.last&.fetch('TextTemplate', nil)
124
+ parameters = array(template&.fetch('Parameters', nil))
125
+ parameters.one? ? parameters.first : nil
126
+ end
127
+
128
+ def primitive_properties
129
+ @values.each_with_object({}) do |(key, (type, value)), result|
130
+ compiled = case type
131
+ when 'Boolean' then value['PrimitiveValue'] == 'true'
132
+ when 'Integer' then value['PrimitiveValue'].to_i
133
+ when 'Enumeration' then value['PrimitiveValue'].to_s
134
+ end
135
+ result[key.to_sym] = compiled unless compiled.nil?
136
+ end
137
+ end
138
+
139
+ def primitive(key)
140
+ pair = @values[key]
141
+ return unless pair
142
+
143
+ pair.last.fetch('PrimitiveValue', nil)
144
+ end
145
+
146
+ def text_value(key)
147
+ pair = @values[key]
148
+ return translated_text(nil) unless pair
149
+
150
+ translated_text(pair.last.fetch('TextTemplate', nil))
151
+ end
152
+
153
+ def image_uri # rubocop:disable Metrics/AbcSize
154
+ pair = @values['imageObject']
155
+ reference = pair ? pair.last.fetch('Image', '').to_s : ''
156
+ module_name, collection_name, image_name = reference.split('.', 3)
157
+ unit = @source.units_of('Images$ImageCollection').find do |candidate|
158
+ candidate.module_name == module_name && candidate.document['Name'] == collection_name
159
+ end
160
+ return unless unit
161
+
162
+ image = array(unit.document['Images']).find { _1['Name'] == image_name }
163
+ return unless image
164
+
165
+ "img/#{[module_name, collection_name, image_name].join('$')}.#{image_format(image)}"
166
+ end # rubocop:enable Metrics/AbcSize
167
+
168
+ def translated_text(template)
169
+ items = template ? array(template.dig('Template', 'Items')) : []
170
+ items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') ||
171
+ items.first&.fetch('Text', '') || ''
172
+ end
173
+
174
+ def widget_key = "#{@key_prefix}.#{@page_name}.#{@widget['Name']}"
175
+
176
+ def css_class
177
+ ["mx-name-#{@widget['Name']}", @widget.dig('Appearance', 'Class')]
178
+ .map(&:to_s).reject(&:empty?).uniq.join(' ')
179
+ end
180
+
181
+ def property_values(object)
182
+ properties = object ? array(object.fetch('Properties', nil)) : []
183
+ properties.filter_map do |property|
184
+ type = @index[IO::BsonCodec.extract_id(property['TypePointer'])]
185
+ next unless type
186
+
187
+ [type.fetch('PropertyKey'), [type.dig('ValueType', 'Type'), property['Value']]]
188
+ end.to_h
189
+ end
190
+
191
+ def widget_type
192
+ object_type_id = IO::BsonCodec.extract_id(@widget.dig('Object', 'TypePointer'))
193
+ @index.values.find do |document|
194
+ IO::BsonCodec.extract_id(document.dig('ObjectType', '$ID')) == object_type_id
195
+ end
196
+ end
197
+
198
+ def document_index
199
+ return @source.document_index if @source.is_a?(SourceModel)
200
+
201
+ {}.tap { |index| @source.documents.each { index_document(_1, index) } }
202
+ end
203
+
204
+ def index_document(value, index)
205
+ case value
206
+ when Hash
207
+ id = IO::BsonCodec.extract_id(value['$ID'])
208
+ index[id] = value if id
209
+ value.each_value { index_document(_1, index) }
210
+ when Array then value.each { index_document(_1, index) }
211
+ end
212
+ end
213
+ end
214
+ # rubocop:enable Metrics
215
+ end
216
+ end