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,614 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'digest'
5
+ require 'json'
6
+
7
+ module Mxrb
8
+ module Compiler
9
+ # Builds the Runtime operation catalog consumed by generated web data sources.
10
+ # rubocop:disable Metrics, Style/HashLikeCase, Style/MultilineBlockChain
11
+ class WebOperationCompiler
12
+ include ModelValues
13
+
14
+ def self.operation_id(page_name, widget_name)
15
+ Base64.strict_encode64(Digest::SHA256.digest("#{page_name}/#{widget_name}").byteslice(0, 16))
16
+ .delete_suffix('==')
17
+ end
18
+
19
+ def self.menu_operation_id(action)
20
+ identifier = IO::BsonCodec.extract_id(action['$ID']).to_s
21
+ identifier = action.dig('MicroflowSettings', 'Microflow').to_s if identifier.empty?
22
+ operation_id('Navigation', identifier)
23
+ end
24
+
25
+ def initialize(source)
26
+ @source = source
27
+ end
28
+
29
+ def write(path)
30
+ operations = @source.units_of('Forms$Page').select { web_page?(_1) }
31
+ .flat_map { page_operations(_1) }
32
+ if @source.is_a?(SourceModel)
33
+ operations.concat(@source.units_of('Forms$Layout').select { web_layout?(_1) }
34
+ .flat_map { page_operations(_1) })
35
+ end
36
+ operations.concat(nanoflow_operations)
37
+ operations = operations.uniq { _1['operationId'] }
38
+ File.write(path, JSON.generate(operations))
39
+ operations
40
+ end
41
+
42
+ private
43
+
44
+ def web_page?(unit) = !@source.is_a?(SourceModel) || @source.web_page?(unit)
45
+ def web_layout?(unit) = !@source.is_a?(SourceModel) || @source.web_layout?(unit)
46
+
47
+ def page_operations(unit)
48
+ page_name = "#{unit.module_name}.#{unit.document['Name']}"
49
+ role_sets = allowed_user_role_sets(unit)
50
+ operations = page_related_documents(unit.document).flat_map do |document|
51
+ custom_widgets(document).filter_map { operation(page_name, _1, role_sets, document) } +
52
+ standard_data_source_widgets(document).filter_map do |widget|
53
+ standard_data_source_operation(page_name, widget, role_sets, document)
54
+ end + data_action_operations(page_name, document, role_sets) + menu_action_operations(document) +
55
+ data_grid_filter_operations(page_name, document, role_sets)
56
+ end
57
+ operations << page_cancel_operation(page_name, role_sets) if popup_page?(unit)
58
+ operations.uniq { _1['operationId'] }
59
+ end
60
+
61
+ def data_grid_filter_operations(page_name, document, role_sets)
62
+ return [] unless @source.is_a?(SourceModel)
63
+
64
+ custom_widgets(document).flat_map do |grid|
65
+ next [] unless custom_widget_id(grid) == DataGridBundleCompiler::WIDGET_ID
66
+
67
+ grid_filter_specs(grid).map do |filter, endpoint, caption|
68
+ {
69
+ 'operationId' => self.class.operation_id(page_name, "#{grid['Name']}$#{filter['Name']}"),
70
+ 'operationType' => 'retrieve', 'parameters' => {},
71
+ 'constants' => {
72
+ 'PageName' => page_name, 'WidgetName' => "#{page_name}.#{grid['Name']}",
73
+ 'UsedAssociations' => [], 'UsedAttributes' => ["#{endpoint}/#{endpoint}.#{caption}"],
74
+ 'XPath' => "//#{endpoint}"
75
+ },
76
+ 'allowedUserRoleSets' => role_sets
77
+ }
78
+ end
79
+ end
80
+ end
81
+
82
+ def grid_filter_specs(grid)
83
+ values = custom_property_values(grid['Object'])
84
+ array(values.dig('columns', 1, 'Objects')).filter_map do |column|
85
+ column_values = custom_property_values(column)
86
+ steps = array(column_values.dig('attribute', 1, 'AttributeRef', 'EntityRef', 'Steps'))
87
+ filter = array(column_values.dig('filter', 1, 'Widgets')).first
88
+ next if steps.empty? || filter.nil?
89
+
90
+ endpoint = steps.last['DestinationEntity'].to_s
91
+ caption = column_values.dig('attribute', 1, 'AttributeRef', 'Attribute').to_s.split('.').last
92
+ [filter, endpoint, caption] unless endpoint.empty? || caption.empty?
93
+ end
94
+ end
95
+
96
+ def custom_widget_id(widget)
97
+ pointer = IO::BsonCodec.extract_id(widget.dig('Object', 'TypePointer'))
98
+ @source.document_index.values.find do |document|
99
+ IO::BsonCodec.extract_id(document.dig('ObjectType', '$ID')) == pointer
100
+ end&.fetch('WidgetId', nil)
101
+ end
102
+
103
+ def custom_property_values(object)
104
+ array(object&.fetch('Properties', nil)).filter_map do |property|
105
+ type = @source.document_index[IO::BsonCodec.extract_id(property['TypePointer'])]
106
+ [type['PropertyKey'], [type.dig('ValueType', 'Type'), property['Value']]] if type
107
+ end.to_h
108
+ end
109
+
110
+ def page_cancel_operation(page_name, role_sets)
111
+ {
112
+ 'operationId' => self.class.operation_id(page_name, '$cancelChanges'),
113
+ 'operationType' => 'rollback', 'parameters' => { 'Objects' => ['AnyObjectList'] },
114
+ 'constants' => {}, 'allowedUserRoleSets' => role_sets
115
+ }
116
+ end
117
+
118
+ def popup_page?(unit)
119
+ return false unless @source.is_a?(SourceModel) && unit.document['$Type'] == 'Forms$Page'
120
+
121
+ name = unit.document.dig('FormCall', 'Form').to_s
122
+ layout = qualified_unit('Forms$Layout', name)
123
+ popup_layout_document?(layout&.document)
124
+ end
125
+
126
+ def popup_layout_document?(document)
127
+ return false unless document
128
+ return true if document['Name'].to_s.match?(/popup/i)
129
+
130
+ document['CanvasWidth'].to_i.positive? && document['CanvasWidth'].to_i <= 800 &&
131
+ menu_widgets(document).empty? && nested(document, 'Forms$Header').empty?
132
+ end
133
+
134
+ def menu_action_operations(document)
135
+ menu_widgets(document).flat_map do |widget|
136
+ resolved_menu_items(widget['MenuSource']).flat_map { menu_item_operations(_1) }
137
+ end
138
+ end
139
+
140
+ def menu_item_operations(item)
141
+ action = item['Action'] || {}
142
+ own = action['$Type'] == 'Forms$MicroflowAction' ? [menu_microflow_operation(action)].compact : []
143
+ own + array(item['Items']).flat_map { menu_item_operations(_1) }
144
+ end
145
+
146
+ def menu_microflow_operation(action)
147
+ name = action.dig('MicroflowSettings', 'Microflow').to_s
148
+ flow = qualified_unit('Microflows$Microflow', name)
149
+ parameters = microflow_parameters(name)
150
+ return unless flow && parameters
151
+
152
+ {
153
+ 'operationId' => self.class.menu_operation_id(action),
154
+ 'operationType' => 'callMicroflow', 'parameters' => parameters,
155
+ 'constants' => { 'MicroflowName' => name },
156
+ 'allowedUserRoleSets' => allowed_user_role_sets(flow)
157
+ }
158
+ end
159
+
160
+ def menu_widgets(document)
161
+ %w[Forms$NavigationTree Forms$MenuBar Forms$SimpleMenuBar].flat_map { nested(document, _1) }
162
+ end
163
+
164
+ def resolved_menu_items(source)
165
+ case source&.fetch('$Type', nil)
166
+ when 'Forms$MenuDocumentSource'
167
+ unit = qualified_unit('Menus$MenuDocument', source['Menu'].to_s)
168
+ array(unit&.document&.dig('ItemCollection', 'Items'))
169
+ when 'Forms$NavigationSource'
170
+ profile = @source.units_of('Navigation$NavigationDocument').flat_map do |unit|
171
+ array(unit.document['Profiles'])
172
+ end.find { _1['Name'] == source['NavigationProfile'] }
173
+ array(profile&.dig('Menu', 'Items'))
174
+ else []
175
+ end
176
+ end
177
+
178
+ def page_related_documents(document, seen = {})
179
+ references = nested(document, 'Forms$SnippetCallWidget').map { _1.dig('FormCall', 'Form').to_s }
180
+ references.filter_map do |qualified|
181
+ next if qualified.empty? || seen[qualified]
182
+
183
+ seen[qualified] = true
184
+ unit = @source.units_of('Forms$Snippet').find do |candidate|
185
+ "#{candidate.module_name}.#{candidate.document['Name']}" == qualified
186
+ end
187
+ unit && page_related_documents(unit.document, seen)
188
+ end.flatten(1).prepend(document)
189
+ end
190
+
191
+ def data_action_operations(page_name, document, role_sets) # rubocop:disable Metrics/MethodLength
192
+ entities = widget_entities(document, document_scope_entity(document))
193
+ action_widgets(document).filter_map do |widget, action|
194
+ type = case action['$Type']
195
+ when 'Forms$SaveChangesClientAction' then 'commit'
196
+ when 'Forms$CancelChangesClientAction' then 'rollback'
197
+ when 'Forms$DeleteClientAction' then 'delete'
198
+ when 'Forms$CreateObjectClientAction' then 'create'
199
+ end
200
+ if type
201
+ entity = entities[widget]
202
+ parameters = if type == 'create'
203
+ {}
204
+ elsif type == 'delete' && !entity.to_s.empty?
205
+ { 'Objects' => ["[#{entity}]"] }
206
+ else
207
+ { 'Objects' => ['AnyObjectList'] }
208
+ end
209
+ constants = type == 'create' ? { 'ObjectType' => action.dig('EntityRef', 'Entity').to_s } : {}
210
+ next({
211
+ 'operationId' => self.class.operation_id(page_name, widget['Name']),
212
+ 'operationType' => type, 'parameters' => parameters,
213
+ 'constants' => constants, 'allowedUserRoleSets' => role_sets
214
+ })
215
+ end
216
+
217
+ microflow_action_operation(page_name, widget, action, role_sets)
218
+ end
219
+ end # rubocop:enable Metrics/MethodLength
220
+
221
+ def action_widgets(document)
222
+ nested(document, 'Forms$ActionButton').map { [_1, _1['Action'] || {}] } +
223
+ nested(document, 'Forms$DivContainer').map { [_1, _1['OnClickAction'] || {}] } +
224
+ custom_widgets(document).flat_map do |widget|
225
+ nested(widget['Object'], 'CustomWidgets$WidgetValue').filter_map do |value|
226
+ action = value['Action'] || {}
227
+ [widget, action] unless action['$Type'].to_s.empty? || action['$Type'] == 'Forms$NoAction'
228
+ end
229
+ end
230
+ end
231
+
232
+ def microflow_action_operation(page_name, widget, action, role_sets)
233
+ return unless action['$Type'] == 'Forms$MicroflowAction'
234
+
235
+ name = action.dig('MicroflowSettings', 'Microflow').to_s
236
+ return if name.empty?
237
+
238
+ parameters = microflow_parameters(name)
239
+ return unless parameters
240
+
241
+ {
242
+ 'operationId' => self.class.operation_id(page_name, widget['Name']),
243
+ 'operationType' => 'callMicroflow', 'parameters' => parameters,
244
+ 'constants' => { 'MicroflowName' => name }, 'allowedUserRoleSets' => role_sets
245
+ }
246
+ end
247
+
248
+ def microflow_parameters(qualified_name)
249
+ flow = @source.units_of('Microflows$Microflow').find do |unit|
250
+ "#{unit.module_name}.#{unit.document['Name']}" == qualified_name
251
+ end
252
+ return {} if flow.nil? && qualified_name == 'System.ShowHomePage'
253
+ return unless flow
254
+
255
+ parameters = nested(flow.document, 'Microflows$MicroflowParameter')
256
+ return unless parameters.all? { supported_microflow_parameter?(_1) }
257
+
258
+ parameters.to_h do |parameter|
259
+ [parameter['Name'].to_s, [parameter.dig('VariableType', 'Entity').to_s]]
260
+ end
261
+ end
262
+
263
+ def supported_microflow_parameter?(parameter)
264
+ type = parameter['VariableType'] || {}
265
+ type['$Type'] == 'DataTypes$ObjectType' && !type['Entity'].to_s.empty?
266
+ end
267
+
268
+ def operation(page_name, widget, role_sets, page_document)
269
+ source = WebListDataSource.new(@source, widget)
270
+ return unless source.supported?
271
+ return xpath_operation(page_name, widget, role_sets, source, page_document) if source.xpath?
272
+ return association_operation(page_name, widget, role_sets, source) if source.association?
273
+ return if source.nanoflow?
274
+
275
+ {
276
+ 'operationId' => self.class.operation_id(page_name, widget['Name']),
277
+ 'operationType' => 'retrieveByMicroflow', 'parameters' => microflow_parameters(source.microflow_name) || {},
278
+ 'constants' => data_source_constants(page_name, widget, source.microflow_name),
279
+ 'allowedUserRoleSets' => role_sets
280
+ }
281
+ end
282
+
283
+ def standard_data_source_widgets(document)
284
+ nested(document, 'Forms$ListView') + nested(document, 'Forms$DataView')
285
+ end
286
+
287
+ def standard_data_source_operation(page_name, widget, role_sets, page_document)
288
+ entities = widget_entities(page_document, document_scope_entity(page_document))
289
+ source_entity = entities[widget]
290
+ return data_view_operation(page_name, widget, role_sets, source_entity) if widget['$Type'] == 'Forms$DataView'
291
+
292
+ source = WebListDataSource.new(@source, widget)
293
+ if source.association?
294
+ return association_retrieve_operation(
295
+ page_name, widget, role_sets, source.association_path, source_entity
296
+ )
297
+ end
298
+ operation(page_name, widget, role_sets, page_document)
299
+ end
300
+
301
+ def data_view_operation(page_name, widget, role_sets, source_entity = nil)
302
+ source = widget['DataSource'] || {}
303
+ if source['$Type'] == 'Forms$MicroflowSource'
304
+ name = source.dig('MicroflowSettings', 'Microflow').to_s
305
+ return unless present_name?(name)
306
+
307
+ return retrieve_by_microflow_operation(page_name, widget, role_sets, name)
308
+ end
309
+ path = entity_ref_path(source['EntityRef'])
310
+ return if path.empty?
311
+
312
+ association_retrieve_operation(page_name, widget, role_sets, path, source_entity)
313
+ end
314
+
315
+ def association_operation(page_name, widget, role_sets, source)
316
+ association_retrieve_operation(page_name, widget, role_sets, source.association_path)
317
+ end
318
+
319
+ def association_retrieve_operation(page_name, widget, role_sets, path, source_entity = nil)
320
+ source_entity ||= association_source_entity(path)
321
+ return unless source_entity
322
+
323
+ destination = path.split('/').last.to_s
324
+ {
325
+ 'operationId' => self.class.operation_id(page_name, widget['Name']),
326
+ 'operationType' => 'retrieve', 'parameters' => { 'CurrentObject' => [source_entity] },
327
+ 'constants' => {
328
+ 'PageName' => page_name, 'WidgetName' => "#{page_name}.#{widget['Name']}",
329
+ 'UsedAssociations' => used_associations(widget, destination),
330
+ 'UsedAttributes' => used_attributes(widget, destination), 'EntityPath' => path
331
+ },
332
+ 'allowedUserRoleSets' => role_sets
333
+ }
334
+ end
335
+
336
+ def call_microflow_operation(page_name, widget, role_sets, name)
337
+ parameters = microflow_parameters(name)
338
+ return unless parameters
339
+
340
+ {
341
+ 'operationId' => self.class.operation_id(page_name, widget['Name']),
342
+ 'operationType' => 'callMicroflow', 'parameters' => parameters,
343
+ 'constants' => { 'MicroflowName' => name }, 'allowedUserRoleSets' => role_sets
344
+ }
345
+ end
346
+
347
+ def retrieve_by_microflow_operation(page_name, widget, role_sets, name)
348
+ parameters = microflow_parameters(name)
349
+ return unless parameters
350
+
351
+ {
352
+ 'operationId' => self.class.operation_id(page_name, widget['Name']),
353
+ 'operationType' => 'retrieveByMicroflow', 'parameters' => parameters,
354
+ 'constants' => data_source_constants(page_name, widget, name),
355
+ 'allowedUserRoleSets' => role_sets
356
+ }
357
+ end
358
+
359
+ def data_source_constants(page_name, widget, microflow_name)
360
+ entity = microflow_return_entity('MicroflowSettings' => { 'Microflow' => microflow_name })
361
+ {
362
+ 'MicroflowName' => microflow_name, 'PageName' => page_name,
363
+ 'WidgetName' => "#{page_name}.#{widget['Name']}",
364
+ 'UsedAssociations' => entity ? used_associations(widget, entity) : [],
365
+ 'UsedAttributes' => entity ? used_attributes(widget, entity) : []
366
+ }
367
+ end
368
+
369
+ def present_name?(value) = value.match?(/\A[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+\z/)
370
+
371
+ def qualified_unit(type, qualified_name)
372
+ @source.units_of(type).find do |unit|
373
+ "#{unit.module_name}.#{unit.document['Name']}" == qualified_name
374
+ end
375
+ end
376
+
377
+ def entity_ref_path(reference)
378
+ array(reference&.fetch('Steps', nil)).flat_map do |step|
379
+ [step['Association'], step['DestinationEntity']]
380
+ end.map(&:to_s).reject(&:empty?).join('/')
381
+ end
382
+
383
+ def association_source_entity(path)
384
+ association_name = path.split('/').first.to_s
385
+ module_name, _, name = association_name.rpartition('.')
386
+ domain = @source.units_of('DomainModels$DomainModel').find { _1.module_name == module_name }
387
+ association = array(domain&.document&.fetch('Associations', nil)).find { _1['Name'] == name }
388
+ pointer = IO::BsonCodec.extract_id(association&.fetch('ParentPointer', nil))
389
+ entity = array(domain&.document&.fetch('Entities', nil)).find do |candidate|
390
+ IO::BsonCodec.extract_id(candidate['$ID']) == pointer
391
+ end
392
+ "#{module_name}.#{entity['Name']}" if entity
393
+ end
394
+
395
+ def used_associations(value, entity)
396
+ attribute_names(value, entity).select { _1.count('/') > 1 }.map do |path|
397
+ path.split('/')[0...-1].join('/')
398
+ end.uniq.sort
399
+ end
400
+
401
+ def widget_entities(document, initial_scope = nil)
402
+ {}.compare_by_identity.tap { collect_widget_entities(document, initial_scope, _1) }
403
+ end
404
+
405
+ def document_scope_entity(document)
406
+ array(document['Parameters']).filter_map do |parameter|
407
+ parameter.dig('ParameterType', 'Entity') || parameter.dig('ParameterType', 'ObjectType', 'Entity')
408
+ end.first
409
+ end
410
+
411
+ def collect_widget_entities(value, scope_entity, result)
412
+ case value
413
+ when Hash
414
+ result[value] = scope_entity if %w[
415
+ Forms$ActionButton Forms$ListView Forms$DataView CustomWidgets$CustomWidget
416
+ ].include?(value['$Type'])
417
+ child_entity = widget_scope_entity(value) || scope_entity
418
+ value.each_value { collect_widget_entities(_1, child_entity, result) }
419
+ when Array then value.each { collect_widget_entities(_1, scope_entity, result) }
420
+ end
421
+ end
422
+
423
+ def widget_scope_entity(widget)
424
+ if %w[Forms$ListView CustomWidgets$CustomWidget].include?(widget['$Type'])
425
+ entity = WebListDataSource.new(@source, widget).entity
426
+ return entity unless entity.empty?
427
+ end
428
+ return unless widget['$Type'] == 'Forms$DataView'
429
+
430
+ source = widget['DataSource'] || {}
431
+ steps = array(source.dig('EntityRef', 'Steps'))
432
+ steps.last&.fetch('DestinationEntity', nil) || source.dig('EntityRef', 'Entity') ||
433
+ microflow_return_entity(source)
434
+ end
435
+
436
+ def microflow_return_entity(source)
437
+ name = source.dig('MicroflowSettings', 'Microflow').to_s
438
+ flow = @source.units_of('Microflows$Microflow').find do |unit|
439
+ "#{unit.module_name}.#{unit.document['Name']}" == name
440
+ end
441
+ flow&.document&.dig('MicroflowReturnType', 'Entity')
442
+ end
443
+
444
+ def nanoflow_operations
445
+ return [] unless @source.is_a?(SourceModel)
446
+
447
+ @source.units_of('Microflows$Nanoflow').flat_map do |unit|
448
+ qualified_name = "#{unit.module_name}.#{unit.document['Name']}"
449
+ nanoflow_action_operations(qualified_name, unit.document)
450
+ end
451
+ end
452
+
453
+ def nanoflow_action_operations(qualified_name, document)
454
+ entities = nanoflow_variable_entities(document)
455
+ nested(document, 'Microflows$ActionActivity').filter_map do |activity|
456
+ action = activity['Action'] || {}
457
+ operation = nanoflow_server_operation(qualified_name, activity, action, entities)
458
+ operation&.merge('allowedUserRoleSets' => [])
459
+ end
460
+ end
461
+
462
+ def nanoflow_server_operation(qualified_name, activity, action, entities)
463
+ operation_id = self.class.operation_id(qualified_name, model_id(activity))
464
+ if action['$Type'] == 'Microflows$MicroflowCallAction'
465
+ name = action.dig('MicroflowCall', 'Microflow').to_s
466
+ parameters = microflow_parameters(name)
467
+ return if name.empty? || !parameters
468
+
469
+ return operation_record(operation_id, 'callMicroflow', parameters, 'MicroflowName' => name)
470
+ end
471
+ return unless commit_action?(action)
472
+
473
+ variable = action['CommitVariableName'] || action['ChangeVariableName'] ||
474
+ action['OutputVariableName'] || action['VariableName']
475
+ entity = entities[variable]
476
+ parameter = entity.to_s.empty? ? 'AnyObjectList' : "[#{entity}]"
477
+ operation_record(operation_id, 'commit', { 'Objects' => [parameter] }, {})
478
+ end
479
+
480
+ def commit_action?(action)
481
+ action['$Type'] == 'Microflows$CommitAction' ||
482
+ %w[Microflows$ChangeAction Microflows$CreateChangeAction].include?(action['$Type']) &&
483
+ action['Commit'] == 'Yes'
484
+ end
485
+
486
+ def operation_record(operation_id, type, parameters, constants)
487
+ {
488
+ 'operationId' => operation_id, 'operationType' => type,
489
+ 'parameters' => parameters, 'constants' => constants
490
+ }
491
+ end
492
+
493
+ def nanoflow_variable_entities(document)
494
+ nested(document, 'Microflows$MicroflowParameter').each_with_object({}) do |parameter, result|
495
+ entity = parameter.dig('VariableType', 'Entity').to_s
496
+ result[parameter['Name']] = entity unless entity.empty?
497
+ end.tap do |result|
498
+ nested(document, 'Microflows$ActionActivity').each do |activity|
499
+ action = activity['Action'] || {}
500
+ variable = action['OutputVariableName'] || action['VariableName']
501
+ entity = action['Entity'].to_s
502
+ result[variable] = entity if !variable.to_s.empty? && !entity.empty?
503
+ end
504
+ end
505
+ end
506
+
507
+ def model_id(value)
508
+ identifier = value.is_a?(Hash) && value.key?('$ID') ? value['$ID'] : value
509
+ IO::BsonCodec.extract_id(identifier).to_s
510
+ end
511
+
512
+ def xpath_operation(page_name, widget, role_sets, source, page_document)
513
+ variables = xpath_variables(source.xpath_constraint)
514
+ page_parameters = object_page_parameters(page_document, variables)
515
+ return unless page_parameters
516
+
517
+ retrieve_operation(page_name, widget, role_sets, source, page_parameters)
518
+ end
519
+
520
+ def retrieve_operation(page_name, widget, role_sets, source, page_parameters)
521
+ {
522
+ 'operationId' => self.class.operation_id(page_name, widget['Name']),
523
+ 'operationType' => 'retrieve',
524
+ 'parameters' => page_parameters.transform_values { [_1] },
525
+ 'constants' => constants(page_name, widget, source.xpath_constraint, source.entity),
526
+ 'allowedUserRoleSets' => role_sets
527
+ }
528
+ end
529
+
530
+ def allowed_user_role_sets(unit) # rubocop:disable Metrics/AbcSize
531
+ module_roles = array(unit.document['AllowedModuleRoles']).map(&:to_s)
532
+ security = @source.documents('Security$ProjectSecurity').first
533
+ array(security&.fetch('UserRoles', nil)).filter_map do |role|
534
+ next if (array(role['ModuleRoles']).map(&:to_s) & module_roles).empty?
535
+
536
+ [role['Name'].to_s]
537
+ end
538
+ end # rubocop:enable Metrics/AbcSize
539
+
540
+ def constants(page_name, widget, constraint, entity)
541
+ {
542
+ 'PageName' => page_name, 'WidgetName' => "#{page_name}.#{widget['Name']}",
543
+ 'UsedAssociations' => [], 'UsedAttributes' => used_attributes(widget, entity),
544
+ 'XPath' => xpath(entity, constraint)
545
+ }
546
+ end
547
+
548
+ def xpath(entity, constraint)
549
+ predicate = constraint.to_s.strip
550
+ return "//#{entity}" if predicate.empty?
551
+
552
+ "//#{entity}#{predicate.start_with?('[') ? predicate : "[#{predicate}]"}"
553
+ end
554
+
555
+ def custom_widgets(value) = nested(value, 'CustomWidgets$CustomWidget')
556
+
557
+ def used_attributes(value, entity)
558
+ attribute_names(value, entity)
559
+ end
560
+
561
+ def attribute_names(value, entity, result = [])
562
+ collect_attribute_names(value, entity, result).uniq.sort
563
+ end
564
+
565
+ def collect_attribute_names(value, entity, result)
566
+ case value
567
+ when Hash then collect_hash_attributes(value, entity, result)
568
+ when Array then value.each { collect_attribute_names(_1, entity, result) }
569
+ when String then collect_string_attributes(value, entity, result)
570
+ end
571
+ result
572
+ end
573
+
574
+ def collect_hash_attributes(value, entity, result)
575
+ path = attribute_inventory_path(value, entity) if value.key?('Attribute')
576
+ result << path if path
577
+ value.each_value { collect_attribute_names(_1, entity, result) }
578
+ end
579
+
580
+ def attribute_inventory_path(reference, entity)
581
+ attribute = reference['Attribute'].to_s
582
+ steps = array(reference.dig('EntityRef', 'Steps'))
583
+ return "#{entity}/#{attribute}" if steps.empty? && attribute.start_with?("#{entity}.")
584
+ return unless qualified_reference_steps?(steps) && attribute.include?('.')
585
+
586
+ path = steps.flat_map { [_1['Association'], _1['DestinationEntity']] }
587
+ ([entity] + path + [attribute]).join('/')
588
+ end
589
+
590
+ def qualified_reference_steps?(steps)
591
+ steps.any? && steps.all? do |step|
592
+ !step['Association'].to_s.empty? && !step['DestinationEntity'].to_s.empty?
593
+ end
594
+ end
595
+
596
+ def collect_string_attributes(value, entity, result)
597
+ value.scan(%r{\$currentObject/([A-Za-z_]\w*)}) do |match|
598
+ result << "#{entity}/#{entity}.#{match.first}"
599
+ end
600
+ end
601
+
602
+ def nested(value, type, result = [])
603
+ case value
604
+ when Hash
605
+ result << value if value['$Type'] == type
606
+ value.each_value { nested(_1, type, result) }
607
+ when Array then value.each { nested(_1, type, result) }
608
+ end
609
+ result
610
+ end
611
+ end
612
+ # rubocop:enable Metrics, Style/HashLikeCase, Style/MultilineBlockChain
613
+ end
614
+ end