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,1716 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "fileutils"
5
+ require "json"
6
+ require "digest"
7
+
8
+ module Mxrb
9
+ # Exports an MPR into an editable, layered Ruby source tree.
10
+ class Exporter
11
+ LAYERS = %w[domain application presentation infrastructure].freeze
12
+ MARKETPLACE_PROVENANCE = [
13
+ ".mxrb/marketplace.lock.json",
14
+ ".mxrb/marketplace",
15
+ ".mxrb/marketplace-originals"
16
+ ].freeze
17
+ EDITABLE_FLOW_OBJECT_TYPES = %w[
18
+ Microflows$StartEvent
19
+ Microflows$EndEvent
20
+ Microflows$ActionActivity
21
+ Microflows$ExclusiveSplit
22
+ Microflows$InheritanceSplit
23
+ Microflows$ExclusiveMerge
24
+ Microflows$LoopedActivity
25
+ Microflows$ErrorEvent
26
+ Microflows$ContinueEvent
27
+ Microflows$Annotation
28
+ Microflows$MicroflowParameter
29
+ ].freeze
30
+ EDITABLE_ACTION_TYPES = %w[
31
+ Microflows$CreateObjectAction
32
+ Microflows$CreateChangeAction
33
+ Microflows$ChangeObjectAction
34
+ Microflows$ChangeAction
35
+ Microflows$RetrieveAction
36
+ Microflows$CommitObjectsAction
37
+ Microflows$CommitAction
38
+ Microflows$DeleteAction
39
+ Microflows$MicroflowCallAction
40
+ Microflows$CreateVariableAction
41
+ Microflows$ChangeVariableAction
42
+ Microflows$ShowMessageAction
43
+ Microflows$LogMessageAction
44
+ Microflows$ShowFormAction
45
+ Microflows$CloseFormAction
46
+ Microflows$JavaActionCallAction
47
+ Microflows$JavaScriptActionCallAction
48
+ Microflows$NanoflowCallAction
49
+ Microflows$AppServiceCallAction
50
+ Microflows$AggregateAction
51
+ Microflows$RollbackAction
52
+ Microflows$CastAction
53
+ Microflows$CreateListAction
54
+ Microflows$ListOperationsAction
55
+ Microflows$ChangeListAction
56
+ Microflows$ValidationFeedbackAction
57
+ Microflows$RestCallAction
58
+ ].freeze
59
+
60
+ def initialize(mpr_path, output_dir)
61
+ @mpr_path = File.expand_path(mpr_path)
62
+ @output_dir = File.expand_path(output_dir)
63
+ end
64
+
65
+ def export!(parallel: true)
66
+ FileUtils.mkdir_p(@output_dir)
67
+ Mxrb.open(@mpr_path) do |project|
68
+ @architecture = project.architecture_definition
69
+ export_app_structure
70
+ export_project_assets
71
+ export_native_units(project)
72
+ export_security(project)
73
+ export_architecture_contracts(project)
74
+ mods = project.modules
75
+ if parallel && mods.size > 1
76
+ threads = mods.map { |mod| Thread.new { export_module(mod) } }
77
+ threads.each(&:join)
78
+ else
79
+ mods.each { export_module(_1) }
80
+ end
81
+ write_project(project)
82
+ end
83
+ @output_dir
84
+ end
85
+
86
+ private
87
+
88
+ def export_module(mod)
89
+ root = File.join(@output_dir, "modules", mod.name)
90
+ LAYERS.each { FileUtils.mkdir_p(File.join(root, _1)) }
91
+
92
+ export_domain(root, mod)
93
+ export_microflows(root, mod)
94
+ export_pages(root, mod)
95
+ export_menus(root, mod)
96
+ export_nanoflows(root, mod)
97
+ export_module_scaffolding(root)
98
+ export_module_security(root, mod)
99
+ write(File.join(root, "module.rb"), module_source(mod))
100
+ end
101
+
102
+ def export_app_structure
103
+ %w[
104
+ app/security app/navigation app/design_system
105
+ theme resources themesource widgets javasource javascriptsource
106
+ ].each { write(File.join(@output_dir, _1, ".keep"), "") }
107
+ end
108
+
109
+ def export_project_assets
110
+ source_root = File.dirname(@mpr_path)
111
+ files = (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory|
112
+ root = File.join(source_root, directory)
113
+ next [root] if File.file?(root) && !File.symlink?(root)
114
+ next [] unless File.directory?(root) && !File.symlink?(root)
115
+
116
+ Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH)
117
+ end.select { File.file?(_1) && !File.symlink?(_1) }.sort
118
+ entries = files.map do |source|
119
+ relative = Pathname.new(source).relative_path_from(Pathname.new(source_root)).to_s
120
+ target = File.join(@output_dir, relative)
121
+ FileUtils.mkdir_p(File.dirname(target))
122
+ FileUtils.cp(source, target)
123
+ {
124
+ "path" => relative,
125
+ "size" => File.size(source),
126
+ "sha256" => Digest::SHA256.file(source).hexdigest
127
+ }
128
+ end
129
+ write(
130
+ File.join(@output_dir, ".mxrb", "assets.json"),
131
+ JSON.pretty_generate("version" => 1, "files" => entries)
132
+ )
133
+ end
134
+
135
+ def export_native_units(project)
136
+ units = project.all_units.filter_map do |unit|
137
+ doc = project.parse_bson(unit)
138
+ type = doc["$Type"]
139
+ next if type.to_s.empty? || type == "Projects$Project"
140
+
141
+ {
142
+ "unit_id" => unit["UnitID"],
143
+ "container_id" => unit["ContainerID"],
144
+ "module" => module_name_for(project, unit),
145
+ "containment" => unit["ContainmentName"],
146
+ "name" => doc["Name"] || doc["name"] || "",
147
+ "type" => type,
148
+ "contents" => Base64.strict_encode64(IO::BsonCodec.serialize(doc))
149
+ }
150
+ end
151
+ write(
152
+ File.join(@output_dir, ".mxrb", "native_units.json"),
153
+ JSON.pretty_generate("format_version" => project.format_version.to_s, "units" => units)
154
+ )
155
+ source = project.all_units.filter_map do |unit|
156
+ doc = project.parse_bson(unit)
157
+ next if doc["$Type"].to_s.empty? || doc["$Type"] == "Projects$Project"
158
+
159
+ native_unit_source(project, unit, doc)
160
+ end
161
+ write(
162
+ File.join(@output_dir, ".mxrb", "native_units.rb"),
163
+ "# frozen_string_literal: true\n\n#{source.join("\n")}"
164
+ )
165
+ end
166
+
167
+ def native_unit_source(project, unit, doc)
168
+ <<~RUBY
169
+ native_unit #{ruby(unit.fetch("UnitID"))},
170
+ container_id: #{ruby(unit.fetch("ContainerID"))},
171
+ containment: #{ruby(unit.fetch("ContainmentName"))},
172
+ module_name: #{ruby(module_name_for(project, unit))},
173
+ deep_structure: #{native_ruby(doc, 20)}
174
+ RUBY
175
+ end
176
+
177
+ def module_name_for(project, unit)
178
+ container = unit["ContainerID"]
179
+ project.modules.find { _1.id == container }&.name ||
180
+ project.modules.find { |mod| native_descendant_of?(project, container, mod.id) }&.name
181
+ end
182
+
183
+ def native_descendant_of?(project, child_id, ancestor_id)
184
+ current = project.raw_unit(child_id)
185
+ until current.nil? || current["UnitID"] == current["ContainerID"]
186
+ return true if current["ContainerID"] == ancestor_id
187
+ current = project.raw_unit(current["ContainerID"])
188
+ end
189
+ false
190
+ end
191
+
192
+ def export_security(project)
193
+ doc = project_security_doc(project)
194
+ source = doc ? security_source(doc) : "# frozen_string_literal: true\n"
195
+ write(File.join(@output_dir, "app", "security", "security.rb"), source)
196
+ end
197
+
198
+ def export_architecture_contracts(project)
199
+ navigation = @architecture&.fetch(:navigation, nil)
200
+ navigation ||= project.navigation.to_h unless project.navigation.empty?
201
+ design_system = @architecture&.fetch(:design_system, nil)
202
+ write(
203
+ File.join(@output_dir, "app", "navigation", "navigation.rb"),
204
+ navigation_source(navigation)
205
+ )
206
+ write(
207
+ File.join(@output_dir, "app", "design_system", "design_system.rb"),
208
+ design_system_source(design_system)
209
+ )
210
+ end
211
+
212
+ def export_module_scaffolding(root)
213
+ %w[
214
+ domain/enumerations domain/rules domain/policies
215
+ application/ports/repositories application/queries application/validations application/jobs
216
+ presentation/features presentation/client_actions presentation/snippets presentation/view_models
217
+ presentation/menus
218
+ infrastructure/persistence/mendix infrastructure/persistence/external
219
+ infrastructure/mappings infrastructure/actions infrastructure/endpoints
220
+ infrastructure/integrations
221
+ security
222
+ ].each { write(File.join(root, _1, ".keep"), "") }
223
+
224
+ security_path = File.join(root, "security", "security.rb")
225
+ write(security_path, "# frozen_string_literal: true\n") unless File.exist?(security_path)
226
+ end
227
+
228
+ def export_module_security(root, mod)
229
+ roles = mod.module_roles.map do |role|
230
+ args = [symbol(role.fetch(:name))]
231
+ description = role.fetch(:description, "")
232
+ args << "description: #{ruby(description)}" unless description.empty?
233
+ "module_role #{args.join(', ')}"
234
+ end
235
+ source = (["# frozen_string_literal: true", ""] + roles).join("\n") + "\n"
236
+ write(File.join(root, "security", "security.rb"), source)
237
+ end
238
+
239
+ def export_domain(root, mod)
240
+ domain = File.join(root, "domain")
241
+ entities_dir = File.join(domain, "entities")
242
+ FileUtils.mkdir_p(entities_dir)
243
+
244
+ associations = mod.associations.group_by(&:from_entity_id)
245
+ architecture = architecture_module(mod.name)
246
+ entity_files = unique_entity_filenames(mod.entities)
247
+ mod.entities.each do |entity|
248
+ entity_metadata = architecture&.fetch(:entities, [])&.find { _1[:name] == entity.name }
249
+ write(
250
+ File.join(entities_dir, entity_files.fetch(entity.id)),
251
+ entity_source(entity, mod, associations.fetch(entity.id, []), entity_metadata)
252
+ )
253
+ end
254
+
255
+ loads = entity_files.values.sort.map do |filename|
256
+ %(evaluate File.join(__dir__, "entities", #{ruby(filename)}))
257
+ end
258
+ write(File.join(domain, "model.rb"), "#{loads.join("\n")}\n")
259
+ end
260
+
261
+ def export_microflows(root, mod)
262
+ files = unique_filenames(mod.microflows)
263
+ by_layer = { "application" => [], "infrastructure" => [] }
264
+ architecture = architecture_module(mod.name)
265
+ mod.microflows.each do |flow|
266
+ layer, category = flow_location(flow.name)
267
+ relative = File.join(category, files.fetch(flow.id))
268
+ metadata = architecture&.fetch(:microflows, [])&.find { _1[:name] == flow.name }
269
+ write(File.join(root, layer, relative), microflow_source(flow, metadata))
270
+ by_layer.fetch(layer) << relative
271
+ end
272
+ repositories = architecture&.fetch(:repositories, []) || []
273
+ unless repositories.empty?
274
+ relative = File.join("ports", "repositories.rb")
275
+ write(File.join(root, "application", relative), repositories_source(repositories))
276
+ by_layer["application"] << relative
277
+ end
278
+ by_layer.each do |layer, paths|
279
+ write_path_aggregator(File.join(root, layer, "#{layer}.rb"), paths)
280
+ end
281
+ end
282
+
283
+ def export_pages(root, mod)
284
+ presentation = File.join(root, "presentation")
285
+ documents = File.join(presentation, "pages")
286
+ FileUtils.mkdir_p(documents)
287
+ files = unique_filenames(mod.pages)
288
+ architecture = architecture_module(mod.name)
289
+ paths = []
290
+ mod.pages.each do |page|
291
+ relative = File.join("pages", files.fetch(page.id))
292
+ metadata = architecture&.fetch(:pages, [])&.find { _1[:name] == page.name }
293
+ write(File.join(presentation, relative), page_source(page, metadata))
294
+ paths << relative
295
+ end
296
+ write_path_aggregator(File.join(presentation, "presentation.rb"), paths)
297
+ end
298
+
299
+ def export_menus(root, mod)
300
+ menus = mod.menus
301
+ return if menus.empty?
302
+
303
+ presentation = File.join(root, "presentation")
304
+ files = unique_filenames(menus)
305
+ paths = []
306
+ menus.each do |menu|
307
+ relative = File.join("menus", files.fetch(menu.id))
308
+ write(File.join(presentation, relative), menu_source(menu))
309
+ paths << relative
310
+ end
311
+ existing_path = File.join(presentation, "presentation.rb")
312
+ existing = File.exist?(existing_path) ? File.read(existing_path).lines : []
313
+ additions = paths.sort.map do |relative|
314
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
315
+ "evaluate File.join(__dir__, #{segments})\n"
316
+ end
317
+ write(existing_path, (existing + additions).join)
318
+ end
319
+
320
+ def export_nanoflows(root, mod)
321
+ flows = mod.nanoflows
322
+ return if flows.empty?
323
+ presentation = File.join(root, "presentation")
324
+ files = unique_filenames(flows)
325
+ paths = []
326
+ architecture = architecture_module(mod.name)
327
+ flows.each do |flow|
328
+ relative = File.join("client_actions", files.fetch(flow.id))
329
+ metadata = architecture&.fetch(:nanoflows, [])&.find { _1[:name] == flow.name }
330
+ write(File.join(presentation, relative), nanoflow_source(flow, metadata))
331
+ paths << relative
332
+ end
333
+ existing = File.read(File.join(presentation, "presentation.rb")).lines
334
+ additions = paths.sort.map do |relative|
335
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
336
+ "evaluate File.join(__dir__, #{segments})\n"
337
+ end
338
+ write(File.join(presentation, "presentation.rb"), (existing + additions).join)
339
+ end
340
+
341
+ def write_path_aggregator(path, relative_paths)
342
+ loads = relative_paths.sort.map do |relative|
343
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
344
+ %(evaluate File.join(__dir__, #{segments}))
345
+ end
346
+ write(path, "#{loads.join("\n")}\n")
347
+ end
348
+
349
+ def flow_location(name)
350
+ case name
351
+ when /\A(?:DS|OQL)_/ then %w[application queries]
352
+ when /\AVAL_/ then %w[application validations]
353
+ when /\ASE_/ then %w[application jobs]
354
+ when /\AAPI_/ then %w[infrastructure endpoints]
355
+ when /\AINT_/ then %w[infrastructure integrations]
356
+ else %w[application use_cases]
357
+ end
358
+ end
359
+
360
+ def write_project(project)
361
+ module_loads = project.modules.map do |mod|
362
+ %( evaluate File.join(__dir__, "modules", #{ruby(mod.name)}, "module.rb"))
363
+ end.join("\n")
364
+
365
+ source = <<~RUBY
366
+ # frozen_string_literal: true
367
+
368
+ require "mxrb"
369
+
370
+ output = ENV.fetch("MXRB_OUTPUT_PATH", File.join(__dir__, #{ruby(File.basename(@mpr_path))}))
371
+
372
+ Mxrb.define(output) do
373
+ mendix_version #{ruby(project.mendix_version || "10.18.0")}
374
+ native_units File.join(__dir__, ".mxrb", "native_units.json")
375
+ project_assets File.join(__dir__, ".mxrb", "assets.json"), root: __dir__
376
+ evaluate File.join(__dir__, ".mxrb", "native_units.rb")
377
+ evaluate File.join(__dir__, "app", "security", "security.rb")
378
+ evaluate File.join(__dir__, "app", "navigation", "navigation.rb")
379
+ evaluate File.join(__dir__, "app", "design_system", "design_system.rb")
380
+ #{module_loads}
381
+ end
382
+ RUBY
383
+ write(File.join(@output_dir, "project.rb"), source)
384
+ end
385
+
386
+ def module_source(mod)
387
+ <<~RUBY
388
+ # frozen_string_literal: true
389
+
390
+ self.module #{symbol(mod.name)} do
391
+ evaluate File.join(__dir__, "domain", "model.rb")
392
+ evaluate File.join(__dir__, "application", "application.rb")
393
+ evaluate File.join(__dir__, "presentation", "presentation.rb")
394
+ evaluate File.join(__dir__, "infrastructure", "infrastructure.rb")
395
+ evaluate File.join(__dir__, "security", "security.rb")
396
+ end
397
+ RUBY
398
+ end
399
+
400
+ def project_security_doc(project)
401
+ raw = project.all_units.find { |unit| project.parse_bson(unit)["$Type"] == "Security$ProjectSecurity" }
402
+ raw && project.parse_bson(raw)
403
+ end
404
+
405
+ def security_source(doc)
406
+ level = doc["SecurityLevel"]
407
+ roles = IO::BsonCodec.parse_array(doc["UserRoles"]).fetch(:items).map do |role|
408
+ module_roles = IO::BsonCodec.parse_array(role["ModuleRoles"]).fetch(:items)
409
+ admin = role["ManageAllRoles"] == true
410
+ args = [symbol(role.fetch("Name"))]
411
+ args << "module_roles: #{ruby(module_roles)}" unless module_roles.empty?
412
+ args << "admin: true" if admin
413
+ " user_role #{args.join(', ')}"
414
+ end
415
+ options = []
416
+ options << " admin_user_role #{ruby(doc["AdminUserRole"])}" unless doc["AdminUserRole"].to_s.empty?
417
+ options << " demo_users #{doc["EnableDemoUsers"] == true}"
418
+ guest = " guest_access #{doc["EnableGuestAccess"] == true}"
419
+ guest += ", role: #{ruby(doc["GuestUserRole"])}" unless doc["GuestUserRole"].to_s.empty?
420
+ options << guest
421
+ options << " sign_in_microflow #{ruby(doc["SignInMicroflow"])}" unless doc["SignInMicroflow"].to_s.empty?
422
+ password = doc["PasswordPolicySettings"]
423
+ if password.is_a?(Hash)
424
+ known = {
425
+ "MinimumLength" => :minimum_length,
426
+ "RequireDigit" => :require_digit,
427
+ "RequireMixedCase" => :require_mixed_case,
428
+ "RequireSymbol" => :require_symbol
429
+ }
430
+ policy = password.each_with_object({}) do |(key, value), result|
431
+ next if %w[$ID $Type].include?(key)
432
+
433
+ result[known.fetch(key, key.to_sym)] = value
434
+ end
435
+ options << " password_policy(**#{ruby(policy)})"
436
+ end
437
+ <<~RUBY
438
+ # frozen_string_literal: true
439
+
440
+ security do
441
+ security_level #{ruby(level)}
442
+ #{roles.join("\n")}
443
+ #{options.join("\n")}
444
+ end
445
+ RUBY
446
+ end
447
+
448
+ def navigation_source(navigation)
449
+ return "# frozen_string_literal: true\n" unless navigation
450
+
451
+ profiles = navigation.fetch(:profiles, []).flat_map { navigation_profile_source(_1) }
452
+ <<~RUBY
453
+ # frozen_string_literal: true
454
+
455
+ navigation do
456
+ #{profiles.join("\n")}
457
+ end
458
+ RUBY
459
+ end
460
+
461
+ def navigation_profile_source(profile)
462
+ args = [symbol(profile.fetch(:name))]
463
+ args << "home_page: #{ruby(profile[:home_page])}" if profile[:home_page]
464
+ args << "home_microflow: #{ruby(profile[:home_microflow])}" if profile[:home_microflow]
465
+ args << "sign_in_page: #{ruby(profile[:sign_in_page])}" if profile[:sign_in_page]
466
+ args << "menu: #{ruby(profile[:menu])}" if profile[:menu]
467
+ role_homes = profile.fetch(:role_homes, {})
468
+ args << "role_homes: #{ruby(role_homes)}" if role_homes.is_a?(Hash) && !role_homes.empty?
469
+ args << "offline: true" if profile[:offline]
470
+ args << "kind: #{ruby(profile[:kind])}" unless profile[:kind].to_s.empty?
471
+ args << "app_icon: #{ruby(profile[:app_icon])}" if profile[:app_icon]
472
+ body = navigation_profile_body(profile, role_homes)
473
+ return [" profile #{args.join(', ')}"] if body.empty?
474
+
475
+ [" profile #{args.join(', ')} do", *body, " end"]
476
+ end
477
+
478
+ def navigation_profile_body(profile, role_homes)
479
+ details = role_homes.is_a?(Array) ? role_homes : profile.fetch(:role_home_details, [])
480
+ titles = profile.fetch(:app_title, {}).map do |locale, text|
481
+ " title #{ruby(locale)}, #{ruby(text)}"
482
+ end
483
+ homes = details.map do |home|
484
+ options = []
485
+ options << "page: #{ruby(home[:page])}" if home[:page]
486
+ options << "microflow: #{ruby(home[:microflow])}" if home[:microflow]
487
+ suffix = options.empty? ? "" : ", #{options.join(', ')}"
488
+ " home_for #{ruby(home.fetch(:role))}#{suffix}"
489
+ end
490
+ items = profile.fetch(:items, []).flat_map { navigation_item_source(_1, 4) }
491
+ titles + homes + items
492
+ end
493
+
494
+ def design_system_source(design_system)
495
+ return "# frozen_string_literal: true\n" unless design_system
496
+
497
+ lines = design_system.fetch(:tokens, []).map do |token|
498
+ args = [symbol(token.fetch(:name))]
499
+ args << "value: #{ruby(token[:value])}" unless token[:value].nil?
500
+ " #{token.fetch(:kind)} #{args.join(', ')}"
501
+ end
502
+ lines.concat(design_system.fetch(:layouts, []).map { " layout #{ruby(_1)}" })
503
+ lines.concat(design_system.fetch(:components, []).map { " component #{ruby(_1)}" })
504
+ lines.concat(design_system.fetch(:accessibility, []).map { " accessibility #{ruby(_1)}" })
505
+ design_system.fetch(:themes, []).each do |theme|
506
+ args = [ruby(theme.fetch(:name))]
507
+ args << "inherits: #{ruby(theme[:inherits])}" if theme[:inherits]
508
+ lines << " theme #{args.join(', ')} do"
509
+ theme.fetch(:tokens, []).each do |token|
510
+ token_args = [symbol(token.fetch(:name))]
511
+ token_args << "value: #{ruby(token[:value])}" unless token[:value].nil?
512
+ lines << " #{token.fetch(:kind)} #{token_args.join(', ')}"
513
+ end
514
+ lines << " end"
515
+ end
516
+ design_system.fetch(:contrast_pairs, []).each do |pair|
517
+ lines << " contrast foreground: #{ruby(pair.fetch(:foreground))}, " \
518
+ "background: #{ruby(pair.fetch(:background))}, level: #{symbol(pair.fetch(:level))}"
519
+ end
520
+ lines << " forbid_literal_colors" if design_system[:forbid_literal_colors]
521
+ <<~RUBY
522
+ # frozen_string_literal: true
523
+
524
+ design_system do
525
+ #{lines.join("\n")}
526
+ end
527
+ RUBY
528
+ end
529
+
530
+ def navigation_item_source(item, indent)
531
+ pad = " " * indent
532
+ caption = item.fetch(:caption)
533
+ primary = caption["en_US"] || caption[:en_US] || caption.values.first || ""
534
+ translations = caption.reject { |locale, _| locale.to_s == "en_US" }
535
+ args = [ruby(primary)]
536
+ args << "page: #{ruby(item[:page])}" if item[:page]
537
+ args << "microflow: #{ruby(item[:microflow])}" if item[:microflow]
538
+ args << "icon: #{ruby(item[:icon])}" if item[:icon]
539
+ args << "translations: #{ruby(translations)}" unless translations.empty?
540
+ children = item.fetch(:items, [])
541
+ return ["#{pad}item #{args.join(', ')}"] if children.empty?
542
+
543
+ ["#{pad}item #{args.join(', ')} do",
544
+ *children.flat_map { navigation_item_source(_1, indent + 2) },
545
+ "#{pad}end"]
546
+ end
547
+
548
+ def entity_source(entity, mod, associations, metadata = nil)
549
+ attrs = entity.attributes.map do |attr|
550
+ options = []
551
+ options << "default: #{ruby(attr.default_value)}" unless attr.default_value.nil? || attr.default_value == ""
552
+ options << "documentation: #{ruby(attr.documentation)}" unless attr.documentation.to_s.empty?
553
+ options << "length: #{attr.length}" unless attr.length.nil?
554
+ options << "localize_date: #{attr.localize_date}" unless attr.localize_date.nil?
555
+ options << "enumeration: #{ruby(attr.enumeration)}" unless attr.enumeration.to_s.empty?
556
+ options << 'required: true' if attr.required
557
+ options << 'unique: true' if attr.unique
558
+ " #{attr.type || :string} #{symbol(attr.name)}#{options.empty? ? '' : ", #{options.join(', ')}"}"
559
+ end
560
+ assocs = associations.map do |assoc|
561
+ target = mod.entities.find { _1.id == assoc.to_entity_id }&.name || assoc.to_entity_id
562
+ qualified_target = target.to_s.include?('.') ? target : "#{mod.name}.#{target}"
563
+ cardinality = association_cardinality(assoc)
564
+ options = ["cardinality: #{symbol(cardinality)}"]
565
+ options << "name: #{ruby(assoc.name)}"
566
+ documentation = assoc.respond_to?(:documentation) ? assoc.documentation : nil
567
+ options << "documentation: #{ruby(documentation)}" unless documentation.to_s.empty?
568
+ inferred_storage = assoc.association_type == :ReferenceSet ? :Table : :Column
569
+ storage_format = assoc.respond_to?(:storage_format) ? assoc.storage_format : nil
570
+ if storage_format && storage_format != inferred_storage
571
+ options << "storage_format: #{symbol(storage_format)}"
572
+ end
573
+ parent_delete = assoc.respond_to?(:parent_delete_behavior) ?
574
+ assoc.parent_delete_behavior : :NoAction
575
+ child_delete = assoc.respond_to?(:child_delete_behavior) ?
576
+ assoc.child_delete_behavior : :NoAction
577
+ options << "parent_delete: #{symbol(parent_delete)}" unless parent_delete == :NoAction
578
+ options << "child_delete: #{symbol(child_delete)}" unless child_delete == :NoAction
579
+ " association #{ruby(qualified_target)}, #{options.join(', ')}"
580
+ end
581
+ flags = []
582
+ flags << " non_persistent!" unless entity.persistable
583
+ flags << " documentation #{ruby(entity.documentation)}" unless entity.documentation.to_s.empty?
584
+ generalization = entity.respond_to?(:generalization_target) ? entity.generalization_target : nil
585
+ flags << " generalizes #{ruby(generalization)}" if generalization
586
+ system_members = entity.respond_to?(:system_members) ? entity.system_members : {}
587
+ system_options = system_members.to_h.select { |_key, value| value }
588
+ unless system_options.empty?
589
+ flags << " system_members #{system_options.map { |key, value| "#{key}: #{value}" }.join(', ')}"
590
+ end
591
+ indexes = entity.respond_to?(:indexes) ? entity.indexes : []
592
+ Array(indexes).each do |index|
593
+ indexed = IO::BsonCodec.parse_array(
594
+ index['attributes'] || index['Attributes'] || index['IndexedAttributes']
595
+ )[:items]
596
+ members = indexed.filter_map { _1['attribute'] || _1['Attribute'] }.map { _1.to_s.split('.').last }
597
+ next if members.empty?
598
+
599
+ directions = indexed.map { |member| member.fetch('ascending', member.fetch('Ascending', true)) }
600
+ options = []
601
+ options << "ascending: #{ruby(directions)}" unless directions.all?
602
+ include_offline = index['includeInOffline'] || index['IncludeInOffline']
603
+ options << 'include_offline: true' if include_offline
604
+ flags << " index #{members.map { symbol(_1) }.join(', ')}#{options.empty? ? '' : ", #{options.join(', ')}"}"
605
+ end
606
+ metadata&.fetch(:lifecycle, [])&.each do |callback|
607
+ flags << " #{callback.fetch(:event)} microflow: #{symbol(callback.fetch(:handler))}"
608
+ end
609
+ access_rules = Array(entity.access_rules)
610
+ access_lines = access_rules.filter_map { |rule| access_rule_source(rule) }
611
+ # A partial access-rule export would replace the complete native rule
612
+ # collection. Keep the collection opaque unless every rule is editable.
613
+ access_lines = [] unless access_lines.size == access_rules.size
614
+ <<~RUBY
615
+ # frozen_string_literal: true
616
+
617
+ entity #{symbol(entity.name)} do
618
+ #{(attrs + assocs + flags + access_lines).join("\n")}
619
+ end
620
+ RUBY
621
+ end
622
+
623
+ def association_cardinality(association)
624
+ return :many_to_many if association.association_type == :ReferenceSet
625
+ return :one_to_one if association.owner == :Both
626
+
627
+ :many_to_one
628
+ end
629
+
630
+ def access_rule_source(rule)
631
+ role_list = rule.fetch(:roles).reject { _1.to_s.empty? }
632
+ return nil if role_list.empty?
633
+ roles = role_list.map { ruby(_1) }.join(", ")
634
+ opts = []
635
+ opts << "create: true" if rule[:create]
636
+ opts << "delete: true" if rule[:delete]
637
+ read_val = reconstruct_access(rule.fetch(:default_rights, "None"), rule.fetch(:members, []), "ReadOnly", "ReadWrite")
638
+ write_val = reconstruct_access(rule.fetch(:default_rights, "None"), rule.fetch(:members, []), "ReadWrite", nil)
639
+ opts << "read: #{format_access(read_val)}" unless read_val == :none
640
+ opts << "write: #{format_access(write_val)}" unless write_val == :none
641
+ opts << "default_rights: #{ruby(rule.fetch(:default_rights, 'None'))}"
642
+ opts << "members: #{native_ruby(rule.fetch(:members, []))}"
643
+ xpath = rule.fetch(:xpath, "")
644
+ opts << "xpath: #{ruby(xpath)}" unless xpath.empty?
645
+ " access_rule #{[roles, *opts].join(', ')}"
646
+ end
647
+
648
+ def reconstruct_access(default_rights, members, primary_rights, secondary_rights)
649
+ if default_rights == "ReadWrite"
650
+ return :all if primary_rights == "ReadWrite"
651
+ return :all if primary_rights == "ReadOnly"
652
+ end
653
+ if default_rights == "ReadOnly"
654
+ return :all if primary_rights == "ReadOnly"
655
+ explicit = members.select { _1[:rights] == primary_rights }.map { _1[:name] }
656
+ return explicit unless explicit.empty?
657
+ return :none
658
+ end
659
+ # default_rights == "None"
660
+ explicit = members.select { |m|
661
+ m[:rights] == primary_rights ||
662
+ (secondary_rights && m[:rights] == secondary_rights)
663
+ }.map { _1[:name] }
664
+ explicit.empty? ? :none : explicit
665
+ end
666
+
667
+ def format_access(value)
668
+ case value
669
+ when :all, :none then ":#{value}"
670
+ when Array then "[#{value.map { symbol(_1) }.join(', ')}]"
671
+ else ":#{value}"
672
+ end
673
+ end
674
+
675
+ def unique_entity_filenames(entities)
676
+ unique_filenames(entities)
677
+ end
678
+
679
+ def unique_filenames(entities)
680
+ used = {}
681
+ entities.to_h do |entity|
682
+ base = underscore(entity.name)
683
+ candidate = "#{base}.rb"
684
+ suffix = 2
685
+ while used[candidate]
686
+ candidate = "#{base}_#{suffix}.rb"
687
+ suffix += 1
688
+ end
689
+ used[candidate] = true
690
+ [entity.id, candidate]
691
+ end
692
+ end
693
+
694
+ def underscore(value)
695
+ value.to_s
696
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
697
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
698
+ .tr(" -", "__")
699
+ .gsub(/[^a-zA-Z0-9_]/, "")
700
+ .downcase
701
+ end
702
+
703
+ def microflow_source(flow, metadata = nil)
704
+ parameters = flow.parameters.filter_map do |param|
705
+ next unless param.is_a?(Hash)
706
+ name = param["Name"] || param["name"]
707
+ type = param["VariableType"] || param["Type"] || param["type"]
708
+ type_source = type.is_a?(Hash) ? native_ruby(type) : symbol(type)
709
+ " parameter #{symbol(name)}, type: #{type_source}" if name && type
710
+ end
711
+ body = parameters
712
+ body << " return_type #{symbol(flow.return_type)}" if flow.return_type.is_a?(String)
713
+ body << " documentation #{ruby(flow.documentation)}" unless flow.documentation.to_s.empty?
714
+ body << " allow_concurrent_execution #{flow.allow_concurrent_execution ? 'true' : 'false'}"
715
+ body << " mark_as_used #{flow.mark_as_used ? 'true' : 'false'}"
716
+ body << " excluded #{flow.excluded ? 'true' : 'false'}"
717
+ body << " allowed_roles #{flow.allowed_module_roles.map { symbol(_1) }.join(', ')}" unless flow.allowed_module_roles.empty?
718
+ metadata&.fetch(:calls, [])&.each do |call|
719
+ body << " call #{call.fetch(:kind)}: #{symbol(call.fetch(:name))}"
720
+ end
721
+ metadata&.fetch(:repositories, [])&.each do |repository|
722
+ body << " uses_repository #{symbol(repository)}"
723
+ end
724
+ objects = Array(flow.objects)
725
+ flows = Array(flow.flows)
726
+ if editable_flow_body?(objects, flows)
727
+ dsl_lines = body_dsl_lines(objects, flows, 2)
728
+ body.concat(dsl_lines)
729
+ fingerprint = flow_body_fingerprint(dsl_lines, flow)
730
+ body << " body_fingerprint #{ruby(fingerprint)}" if fingerprint
731
+ elsif objects.any?
732
+ body << " # Native body baseline retained: this graph shape has no typed DSL mapping yet"
733
+ end
734
+ <<~RUBY
735
+ # frozen_string_literal: true
736
+
737
+ microflow #{symbol(flow.name)} do
738
+ #{body.join("\n")}
739
+ end
740
+ RUBY
741
+ end
742
+
743
+ def nanoflow_source(flow, metadata = nil)
744
+ microflow_source(flow, metadata).sub(/(^\s*)microflow /, '\1nanoflow ')
745
+ end
746
+
747
+ def flow_body_fingerprint(lines, flow)
748
+ builder = Dsl::FlowBuilder.new(
749
+ flow.name, runtime: :server, kind: :microflow, public: false
750
+ )
751
+ builder.instance_eval(lines.join("\n"), @mpr_path, 1)
752
+ definition = builder.to_h
753
+ Dsl::FlowBuilder.body_digest(
754
+ definition[:body], definition[:return_expression]
755
+ )
756
+ rescue StandardError, SyntaxError
757
+ nil
758
+ end
759
+
760
+ def repositories_source(repositories)
761
+ repositories.map do |repository|
762
+ options = []
763
+ options << "implementation: #{symbol(repository[:implementation])}" if repository[:implementation]
764
+ options << "public: true" if repository[:public]
765
+ options << "documentation: #{ruby(repository[:documentation])}" unless repository[:documentation].to_s.empty?
766
+ "repository #{symbol(repository.fetch(:name))}#{options.empty? ? '' : ", #{options.join(', ')}"}"
767
+ end.join("\n") + "\n"
768
+ end
769
+
770
+ def page_source(page, metadata = nil)
771
+ body = []
772
+ body << " layout #{ruby(page.layout_id)}" if page.layout_id
773
+ body << " title #{ruby(page.title)}"
774
+ body << " popup!" if page.popup_width.to_i.positive? || page.popup_height.to_i.positive?
775
+ body << " allowed_roles #{page.allowed_module_roles.map { symbol(_1) }.join(', ')}" unless page.allowed_module_roles.empty?
776
+ deep = page_deep_structure(page)
777
+ if (source = metadata&.dig(:data_source) || page.data_source)
778
+ body << " data_source #{source.fetch(:kind)}: #{symbol(source.fetch(:name))}"
779
+ end
780
+ body << " deep_structure(#{native_ruby(deep, 2)})" if deep
781
+ metadata&.fetch(:events, [])&.each do |event|
782
+ args = []
783
+ args << "target: #{symbol(event[:target])}" if event[:target]
784
+ args << "#{event.fetch(:kind)}: #{symbol(event.fetch(:handler))}"
785
+ body << " #{event.fetch(:event)} #{args.join(', ')}"
786
+ end
787
+ widgets = metadata ? metadata.fetch(:widgets, []) : page.widgets
788
+ widgets.each do |widget|
789
+ rendered = render_widget(widget, 2)
790
+ body.concat(rendered)
791
+ end
792
+ <<~RUBY
793
+ # frozen_string_literal: true
794
+
795
+ page #{symbol(page.name)} do
796
+ #{body.join("\n")}
797
+ end
798
+ RUBY
799
+ end
800
+
801
+ PAGE_TYPED_KEYS = %w[$ID Name name].freeze
802
+ def page_deep_structure(page)
803
+ raw = page.raw_document
804
+ return nil unless raw.is_a?(Hash)
805
+
806
+ raw.reject { |key, _| PAGE_TYPED_KEYS.include?(key.to_s) }
807
+ end
808
+
809
+ def native_ruby(value, indent = 0)
810
+ case value
811
+ when BSON::Binary
812
+ encoded = Base64.strict_encode64(value.data)
813
+ "bson_binary(#{ruby(encoded)}, subtype: #{value.type.inspect})"
814
+ when Time
815
+ "Time.at(#{value.to_i}, #{value.nsec}, :nanosecond).utc"
816
+ when Hash
817
+ return "{}" if value.empty?
818
+
819
+ pad = " " * indent
820
+ child_pad = " " * (indent + 2)
821
+ entries = value.map do |key, item|
822
+ "#{child_pad}#{ruby(key)} => #{native_ruby(item, indent + 2)}"
823
+ end
824
+ "{\n#{entries.join(",\n")}\n#{pad}}"
825
+ when Array
826
+ return "[]" if value.empty?
827
+
828
+ pad = " " * indent
829
+ child_pad = " " * (indent + 2)
830
+ entries = value.map { "#{child_pad}#{native_ruby(_1, indent + 2)}" }
831
+ "[\n#{entries.join(",\n")}\n#{pad}]"
832
+ else
833
+ ruby(value)
834
+ end
835
+ end
836
+
837
+ def menu_source(menu)
838
+ body = menu.items.flat_map { menu_item_source(_1, 2) }
839
+ if menu.raw_document.is_a?(Hash)
840
+ deep = menu.raw_document.reject { |key, _| %w[$ID Name name].include?(key.to_s) }
841
+ body.unshift(" deep_structure(#{native_ruby(deep, 2)})")
842
+ end
843
+ <<~RUBY
844
+ # frozen_string_literal: true
845
+
846
+ menu #{symbol(menu.name)} do
847
+ #{body.join("\n")}
848
+ end
849
+ RUBY
850
+ end
851
+
852
+ def menu_item_source(item, indent)
853
+ pad = " " * indent
854
+ args = [ruby(item.fetch(:caption))]
855
+ args << "page: #{ruby(item[:page])}" if item[:page]
856
+ children = Array(item[:items])
857
+ return ["#{pad}item #{args.join(', ')}"] if children.empty?
858
+
859
+ lines = ["#{pad}item #{args.join(', ')} do"]
860
+ lines.concat(children.flat_map { menu_item_source(_1, indent + 2) })
861
+ lines << "#{pad}end"
862
+ end
863
+
864
+ def render_widget(widget, indent)
865
+ pad = " " * indent
866
+ child_pad = " " * (indent + 2)
867
+ type = widget.fetch(:type)
868
+ options = widget.fetch(:options, {})
869
+
870
+ # Snippet: snippet :name, from: "..."
871
+ if type == :snippet
872
+ snippet_ref = options[:snippet].to_s
873
+ args = [ruby(widget.fetch(:name))]
874
+ args << "from: #{ruby(snippet_ref)}" unless snippet_ref == widget.fetch(:name).to_s
875
+ return ["#{pad}snippet #{args.join(', ')}"]
876
+ end
877
+
878
+ if type == :native_widget
879
+ return [
880
+ "#{pad}native_widget #{symbol(widget.fetch(:name))},",
881
+ "#{child_pad}type: #{ruby(options.fetch(:native_type))},",
882
+ "#{child_pad}deep_structure: #{native_ruby(options.fetch(:deep_structure), indent + 2)}"
883
+ ]
884
+ end
885
+
886
+ if type == :pluggable_widget
887
+ args = [symbol(widget.fetch(:name)), "widget_id: #{ruby(options.fetch(:widget_id))}"]
888
+ args << "widget_name: #{ruby(options[:widget_name])}" if options[:widget_name]
889
+ args << "properties: #{native_ruby(options[:properties])}" if options[:properties]
890
+ args << "class_name: #{ruby(options[:class])}" if options[:class]
891
+ return ["#{pad}pluggable_widget #{args.join(', ')}"]
892
+ end
893
+
894
+ # drop_down
895
+ if type == :drop_down
896
+ args = [symbol(widget.fetch(:name))]
897
+ args << "attribute: #{symbol(options[:attribute])}" if options[:attribute]
898
+ args << "caption: #{ruby(options[:caption])}" if options.key?(:caption) && !options[:caption].nil?
899
+ return ["#{pad}drop_down #{args.join(', ')}"]
900
+ end
901
+
902
+ # Container: recurse into children
903
+ if type == :container
904
+ children_lines = Array(widget[:children]).flat_map { render_widget(_1, indent + 2) }
905
+ args = [symbol(widget.fetch(:name))]
906
+ args << "class_name: #{ruby(options[:class])}" if options[:class]
907
+ if children_lines.empty?
908
+ return ["#{pad}container #{args.join(', ')}"]
909
+ else
910
+ lines = ["#{pad}container #{args.join(', ')} do"]
911
+ lines.concat(children_lines)
912
+ lines << "#{pad}end"
913
+ return lines
914
+ end
915
+ end
916
+
917
+ args = [symbol(widget.fetch(:name))]
918
+ args << "attribute: #{symbol(options[:attribute])}" if options[:attribute]
919
+ args << "caption: #{ruby(options[:caption])}" if options.key?(:caption) && !options[:caption].nil?
920
+ args << "entity: #{ruby(options[:entity])}" if options[:entity]
921
+ args << "lines: #{options[:lines]}" if type == :text_area && options[:lines]
922
+ if type == :reference_selector && options[:display_attribute]
923
+ args << "display_attribute: #{ruby(options[:display_attribute])}"
924
+ end
925
+
926
+ widget_body = []
927
+ Array(options[:columns]).each do |column|
928
+ column_args = [symbol(column.fetch(:name))]
929
+ column_args << "attribute: #{symbol(column[:attribute])}" if column[:attribute]
930
+ column_args << "caption: #{ruby(column[:caption])}" if column.key?(:caption) && !column[:caption].nil?
931
+ widget_body << "#{child_pad}column #{column_args.join(', ')}"
932
+ end
933
+ Array(options[:tabs]).each do |tab|
934
+ tab_args = [symbol(tab.fetch(:name))]
935
+ tab_args << "caption: #{ruby(tab[:caption])}" if tab.key?(:caption) && !tab[:caption].nil?
936
+ tab_widgets = Array(tab[:widgets])
937
+ if tab_widgets.empty?
938
+ widget_body << "#{child_pad}tab_page #{tab_args.join(', ')}"
939
+ else
940
+ widget_body << "#{child_pad}tab_page #{tab_args.join(', ')} do"
941
+ widget_body.concat(tab_widgets.flat_map { render_widget(_1, indent + 4) })
942
+ widget_body << "#{child_pad}end"
943
+ end
944
+ end
945
+
946
+ if (sb = options[:search_bar])
947
+ sb_lines = ["#{child_pad}search_bar do"]
948
+ Array(sb[:fields]).each do |field|
949
+ sf_args = [symbol(field[:attribute])]
950
+ sf_args << "caption: #{ruby(field[:caption])}" if field[:caption]
951
+ sb_lines << "#{child_pad} search_field #{sf_args.join(', ')}"
952
+ end
953
+ sb_lines << "#{child_pad}end"
954
+ widget_body.concat(sb_lines)
955
+ end
956
+
957
+ if (tb = options[:toolbar])
958
+ tb_lines = ["#{child_pad}toolbar do"]
959
+ Array(tb[:buttons]).each do |btn|
960
+ btn_method = case btn[:type].to_sym
961
+ when :new then "new_button"
962
+ when :delete then "delete_button"
963
+ when :search then "search_button"
964
+ when :export then "export_button"
965
+ end
966
+ next unless btn_method
967
+ tb_arg = btn[:caption] ? "caption: #{ruby(btn[:caption])}" : nil
968
+ tb_lines << "#{child_pad} #{btn_method}#{tb_arg ? "(#{tb_arg})" : ""}"
969
+ end
970
+ tb_lines << "#{child_pad}end"
971
+ widget_body.concat(tb_lines)
972
+ end
973
+
974
+ widget.fetch(:events, []).each do |event|
975
+ widget_body << "#{child_pad}#{event.fetch(:event)} #{event.fetch(:kind)}: #{symbol(event.fetch(:handler))}"
976
+ end
977
+
978
+ if widget_body.empty?
979
+ ["#{pad}#{type} #{args.join(', ')}"]
980
+ else
981
+ lines = ["#{pad}#{type} #{args.join(', ')} do"]
982
+ lines.concat(widget_body)
983
+ lines << "#{pad}end"
984
+ end
985
+ end
986
+
987
+ # ── Microflow / nanoflow body export ─────────────────────────────────────
988
+
989
+ def editable_flow_body?(objects, flows, nested: false)
990
+ return nested if objects.empty?
991
+ local_flows = flows_for_objects(flows, objects)
992
+ unless nested
993
+ return false unless objects.count { _1["$Type"] == "Microflows$StartEvent" } == 1
994
+ return false unless objects.any? { _1["$Type"] == "Microflows$EndEvent" }
995
+ end
996
+ connected_objects = objects.reject do |object|
997
+ %w[Microflows$Annotation Microflows$MicroflowParameter].include?(object["$Type"])
998
+ end
999
+ return false if connected_objects.size > 1 && local_flows.empty?
1000
+ return false unless objects.all? { EDITABLE_FLOW_OBJECT_TYPES.include?(_1["$Type"]) }
1001
+ return false unless local_flows.all? do
1002
+ %w[Microflows$SequenceFlow Microflows$AnnotationFlow].include?(_1["$Type"])
1003
+ end
1004
+ sequence_flows = local_flows.select { _1["$Type"] == "Microflows$SequenceFlow" }
1005
+ return false if sequence_flows.select { _1["IsErrorHandler"] == true }
1006
+ .group_by { _1["OriginPointer"] }.any? { |_origin, items| items.size > 1 }
1007
+
1008
+ by_id = objects.to_h { [_1["$ID"], _1] }
1009
+
1010
+ objects.all? do |object|
1011
+ case object["$Type"]
1012
+ when "Microflows$ActionActivity"
1013
+ editable_action?(object["Action"] || {})
1014
+ when "Microflows$ExclusiveSplit"
1015
+ decision_shape_editable?(object, sequence_flows, by_id)
1016
+ when "Microflows$InheritanceSplit"
1017
+ branches = sequence_flows.select { _1["OriginPointer"] == object["$ID"] }
1018
+ !object["SplitVariableName"].to_s.empty? &&
1019
+ branches.size >= 2 &&
1020
+ branches.all? { by_id.key?(_1["DestinationPointer"]) }
1021
+ when "Microflows$LoopedActivity"
1022
+ collection = object["ObjectCollection"] || {}
1023
+ inner_objects = bson_items(collection["Objects"])
1024
+ source = loop_source(object)
1025
+ return false unless %w[
1026
+ Microflows$WhileLoopCondition Microflows$IterableList
1027
+ Microflows$IteratorList
1028
+ ].include?(source["$Type"])
1029
+ editable_flow_body?(
1030
+ inner_objects, flows, nested: true
1031
+ )
1032
+ else
1033
+ true
1034
+ end
1035
+ end
1036
+ end
1037
+
1038
+ def editable_action?(action)
1039
+ return false unless EDITABLE_ACTION_TYPES.include?(action["$Type"])
1040
+
1041
+ case action["$Type"]
1042
+ when "Microflows$ChangeObjectAction"
1043
+ action.fetch("Commit", "No") == "No" && action.fetch("RefreshInClient", false) == false
1044
+ when "Microflows$CreateChangeAction", "Microflows$ChangeAction"
1045
+ %w[No Yes YesWithoutEvents].include?(action.fetch("Commit", "No")) &&
1046
+ [true, false].include?(action.fetch("RefreshInClient", false))
1047
+ when "Microflows$RetrieveAction"
1048
+ source = action["RetrieveSource"] || {}
1049
+ case source["$Type"]
1050
+ when "Microflows$DatabaseRetrieveSource"
1051
+ range_type = source.dig("Range", "$Type")
1052
+ %w[Microflows$ConstantRange Microflows$CustomRange].include?(range_type) &&
1053
+ bson_items(source.dig("NewSortings", "Sortings") || source["SortItems"]).all? do |sorting|
1054
+ !(sorting["AttributePath"] || sorting.dig("AttributeRef", "Attribute")).to_s.empty?
1055
+ end
1056
+ when "Microflows$AssociationRetrieveSource"
1057
+ !source["AssociationId"].to_s.empty? && !source["StartVariableName"].to_s.empty?
1058
+ else
1059
+ false
1060
+ end
1061
+ when "Microflows$ShowMessageAction"
1062
+ bson_items(action.dig("Template", "Parameters")).all? { _1["Expression"] }
1063
+ when "Microflows$LogMessageAction"
1064
+ bson_items(action.dig("MessageTemplate", "Parameters")).all? { _1["Expression"] }
1065
+ when "Microflows$ShowFormAction"
1066
+ settings = action["FormSettings"] || {}
1067
+ title = settings["FormTitle"] || settings["TitleOverride"]
1068
+ title_supported = title.nil? ||
1069
+ (title.is_a?(Hash) && bson_items(title["Parameters"]).empty?)
1070
+ title_supported && bson_items(settings["ParameterMappings"]).all? do |mapping|
1071
+ mapping["Parameter"] && mapping["Argument"]
1072
+ end
1073
+ when "Microflows$JavaActionCallAction"
1074
+ !action["JavaAction"].to_s.empty? && bson_items(action["ParameterMappings"]).all? do |mapping|
1075
+ value = mapping["Value"] || {}
1076
+ mapping["Parameter"] && %w[
1077
+ Microflows$BasicJavaActionParameterValue
1078
+ Microflows$BasicCodeActionParameterValue
1079
+ Microflows$EntityTypeJavaActionParameterValue
1080
+ Microflows$MicroflowJavaActionParameterValue
1081
+ Microflows$ImportMappingJavaActionParameterValue
1082
+ Microflows$ExportMappingJavaActionParameterValue
1083
+ ].include?(value["$Type"])
1084
+ end
1085
+ when "Microflows$JavaScriptActionCallAction"
1086
+ !action["JavaScriptAction"].to_s.empty? && bson_items(action["ParameterMappings"]).all? do |mapping|
1087
+ value = mapping["ParameterValue"] || {}
1088
+ mapping["Parameter"] && %w[
1089
+ Microflows$BasicCodeActionParameterValue
1090
+ Microflows$EntityTypeCodeActionParameterValue
1091
+ ].include?(value["$Type"])
1092
+ end
1093
+ when "Microflows$NanoflowCallAction"
1094
+ !action.dig("NanoflowCall", "Nanoflow").to_s.empty? &&
1095
+ bson_items(action.dig("NanoflowCall", "ParameterMappings")).all? do |mapping|
1096
+ mapping["Parameter"] && mapping["Argument"]
1097
+ end
1098
+ when "Microflows$AppServiceCallAction"
1099
+ !action["AppServiceAction"].to_s.empty? && bson_items(action["ParameterMappings"]).all? do |mapping|
1100
+ mapping["Parameter"] && mapping["Argument"]
1101
+ end
1102
+ when "Microflows$ValidationFeedbackAction"
1103
+ bson_items(action.dig("FeedbackTemplate", "Parameters")).all? do |parameter|
1104
+ parameter["Expression"]
1105
+ end
1106
+ when "Microflows$RestCallAction"
1107
+ http = action["HttpConfiguration"] || {}
1108
+ request = action["RequestHandling"] || {}
1109
+ !http["HttpMethod"].to_s.empty? &&
1110
+ bson_items(http["HttpHeaderEntries"]).all? { _1["Key"] && _1["Value"] } &&
1111
+ bson_items(http.dig("CustomLocationTemplate", "Parameters")).all? {
1112
+ _1["Expression"]
1113
+ } &&
1114
+ request["$Type"] == "Microflows$MappingRequestHandling"
1115
+ else
1116
+ true
1117
+ end
1118
+ end
1119
+
1120
+ def decision_shape_editable?(object, flows, by_id)
1121
+ condition = object["SplitCondition"] || {}
1122
+ condition_supported = !condition["Expression"].to_s.empty? ||
1123
+ (condition["$Type"] == "Microflows$RuleSplitCondition" &&
1124
+ !condition.dig("RuleCall", "Microflow").to_s.empty? &&
1125
+ bson_items(condition.dig("RuleCall", "ParameterMappings")).all? {
1126
+ _1["Parameter"] && _1["Argument"]
1127
+ })
1128
+ return false unless condition_supported
1129
+
1130
+ branches = flows.reject { _1["IsErrorHandler"] == true }
1131
+ .select { _1["OriginPointer"] == object["$ID"] }
1132
+ branches.size >= 2 && branches.all? { by_id.key?(_1["DestinationPointer"]) }
1133
+ end
1134
+
1135
+ def body_dsl_lines(objects, flows, indent = 2, nested: false)
1136
+ return [] if objects.empty?
1137
+ by_id = objects.to_h { |o| [o["$ID"], o] }
1138
+ fwd_map = {}
1139
+ err_map = {}
1140
+ flows.each do |f|
1141
+ next unless f["$Type"] == "Microflows$SequenceFlow"
1142
+
1143
+ from = f["OriginPointer"]
1144
+ to = f["DestinationPointer"]
1145
+ case_val = flow_case_value(f)
1146
+ if f["IsErrorHandler"] == true
1147
+ err_map[from] = to
1148
+ else
1149
+ fwd_map[from] ||= []
1150
+ fwd_map[from] << { to: to, case_val: case_val }
1151
+ end
1152
+ end
1153
+ start = objects.find { |o| o["$Type"] == "Microflows$StartEvent" }
1154
+ if nested && !start
1155
+ destinations = flows.to_h { [_1["DestinationPointer"], true] }
1156
+ start = objects.find { !destinations.key?(_1["$ID"]) }
1157
+ end
1158
+ return [] unless start
1159
+ lines = []
1160
+ linearize_flow(start["$ID"], fwd_map, err_map, by_id, flows, {}, lines, indent, nil)
1161
+ lines
1162
+ end
1163
+
1164
+ def flow_case_value(flow)
1165
+ case_doc = bson_items(flow["CaseValues"]).first || flow["NewCaseValue"]
1166
+ value = case_doc&.dig("Value")
1167
+ return true if value == "true"
1168
+ return false if value == "false"
1169
+
1170
+ value
1171
+ end
1172
+
1173
+ def linearize_flow(cursor, fwd, err, by_id, graph_flows, seen, lines, indent, stop_at)
1174
+ while cursor && cursor != stop_at
1175
+ break if seen[cursor]
1176
+ seen[cursor] = true
1177
+ obj = by_id[cursor]
1178
+ break unless obj
1179
+
1180
+ case obj["$Type"]
1181
+ when "Microflows$StartEvent"
1182
+ cursor = (fwd[cursor] || []).first&.dig(:to)
1183
+
1184
+ when "Microflows$ActionActivity"
1185
+ line = action_dsl_line(obj, indent)
1186
+ lines << line if line
1187
+ next_id = (fwd[cursor] || []).first&.dig(:to)
1188
+ error_to = err[cursor]
1189
+ if error_to
1190
+ rescue_lines = []
1191
+ linearize_flow(
1192
+ error_to, fwd, err, by_id, graph_flows,
1193
+ seen.dup, rescue_lines, indent + 2, nil
1194
+ )
1195
+ lines << "#{' ' * indent}rescue_all do"
1196
+ lines.concat(rescue_lines)
1197
+ lines << "#{' ' * indent}end"
1198
+ end
1199
+ cursor = next_id
1200
+
1201
+ when "Microflows$ExclusiveSplit"
1202
+ condition_doc = obj["SplitCondition"] || {}
1203
+ condition = if condition_doc["$Type"] == "Microflows$RuleSplitCondition"
1204
+ rule = condition_doc["RuleCall"] || {}
1205
+ mappings = bson_items(rule["ParameterMappings"]).to_h do |mapping|
1206
+ [mapping["Parameter"].to_s, mapping["Argument"].to_s]
1207
+ end
1208
+ { rule: rule["Microflow"], pass: mappings }
1209
+ else
1210
+ condition_doc["Expression"].to_s
1211
+ end
1212
+ nexts = fwd[cursor] || []
1213
+ merge_id = find_common_merge_node(nexts.map { _1[:to] }, fwd, by_id)
1214
+ rendered_condition = condition.is_a?(Hash) ?
1215
+ "(#{ruby(condition)})" : ruby(condition)
1216
+ lines << "#{' ' * indent}decision #{rendered_condition} do"
1217
+ nexts.each do |edge|
1218
+ branch_lines = []
1219
+ linearize_flow(
1220
+ edge[:to], fwd, err, by_id, graph_flows, seen.dup,
1221
+ branch_lines, indent + 4, merge_id
1222
+ )
1223
+ lines << "#{' ' * (indent + 2)}on(#{ruby(edge[:case_val])}) do"
1224
+ lines.concat(branch_lines)
1225
+ lines << "#{' ' * (indent + 2)}end"
1226
+ end
1227
+ lines << "#{' ' * indent}end"
1228
+ cursor = merge_id
1229
+
1230
+ when "Microflows$ExclusiveMerge"
1231
+ cursor = (fwd[cursor] || []).first&.dig(:to)
1232
+
1233
+ when "Microflows$InheritanceSplit"
1234
+ nexts = fwd[cursor] || []
1235
+ merge_id = find_common_merge_node(nexts.map { _1[:to] }, fwd, by_id)
1236
+ lines << "#{' ' * indent}type_decision :#{obj["SplitVariableName"]} do"
1237
+ nexts.each do |edge|
1238
+ branch_lines = []
1239
+ linearize_flow(
1240
+ edge[:to], fwd, err, by_id, graph_flows, seen.dup,
1241
+ branch_lines, indent + 4, merge_id
1242
+ )
1243
+ if edge[:case_val].to_s.empty?
1244
+ lines << "#{' ' * (indent + 2)}otherwise do"
1245
+ else
1246
+ lines << "#{' ' * (indent + 2)}on_type #{ruby(edge[:case_val])} do"
1247
+ end
1248
+ lines.concat(branch_lines)
1249
+ lines << "#{' ' * (indent + 2)}end"
1250
+ end
1251
+ lines << "#{' ' * indent}end"
1252
+ cursor = merge_id
1253
+
1254
+ when "Microflows$LoopedActivity"
1255
+ loop_src = loop_source(obj)
1256
+ variable = (loop_src["ListVariableName"] || loop_src["Variable"]).to_s
1257
+ iterator = (loop_src["VariableName"] || loop_src["IteratorVariable"]).to_s
1258
+ inner_col = obj["ObjectCollection"] || {}
1259
+ inner_objects = bson_items(inner_col["Objects"])
1260
+ inner_lines = body_dsl_lines(
1261
+ inner_objects,
1262
+ graph_flows,
1263
+ indent + 2,
1264
+ nested: true
1265
+ )
1266
+ if loop_src["$Type"] == "Microflows$WhileLoopCondition"
1267
+ lines << "#{' ' * indent}while_loop #{ruby(loop_src["WhileExpression"])} do"
1268
+ else
1269
+ as_clause = (iterator != variable && !iterator.empty?) ? ", as: :#{iterator}" : ""
1270
+ lines << "#{' ' * indent}loop_over :#{variable}#{as_clause} do"
1271
+ end
1272
+ lines.concat(inner_lines)
1273
+ lines << "#{' ' * indent}end"
1274
+ cursor = (fwd[cursor] || []).first&.dig(:to)
1275
+
1276
+ when "Microflows$EndEvent"
1277
+ ret = obj["ReturnValue"].to_s.strip
1278
+ unless ret.empty?
1279
+ if (match = ret.match(/\A\$([A-Za-z_][A-Za-z0-9_]*)\z/))
1280
+ lines << "#{' ' * indent}return_value :#{match[1]}"
1281
+ else
1282
+ lines << "#{' ' * indent}return_value #{ruby(ret)}"
1283
+ end
1284
+ else
1285
+ lines << "#{' ' * indent}end_flow" if indent > 2
1286
+ end
1287
+ break
1288
+
1289
+ when "Microflows$ErrorEvent"
1290
+ lines << "#{' ' * indent}error_event"
1291
+ break
1292
+
1293
+ when "Microflows$ContinueEvent"
1294
+ lines << "#{' ' * indent}continue_loop"
1295
+ break
1296
+
1297
+ else
1298
+ cursor = (fwd[cursor] || []).first&.dig(:to)
1299
+ end
1300
+ end
1301
+ end
1302
+
1303
+ def flows_for_objects(flows, objects)
1304
+ ids = objects.to_h { [_1["$ID"], true] }
1305
+ flows.select do |flow|
1306
+ ids.key?(flow["OriginPointer"]) && ids.key?(flow["DestinationPointer"])
1307
+ end
1308
+ end
1309
+
1310
+ def loop_source(object)
1311
+ object["LoopSource"] || object["LoopObject"] || begin
1312
+ if object["ListVariableName"] || object["IteratorVariableName"]
1313
+ {
1314
+ "$Type" => "Microflows$IterableList",
1315
+ "ListVariableName" => object["ListVariableName"],
1316
+ "VariableName" => object["IteratorVariableName"]
1317
+ }
1318
+ else
1319
+ {}
1320
+ end
1321
+ end
1322
+ end
1323
+
1324
+ def find_merge_node(true_start, false_start, fwd, by_id)
1325
+ t = reachable_merge_ids(true_start, fwd, by_id)
1326
+ f = reachable_merge_ids(false_start, fwd, by_id)
1327
+ (t & f).first
1328
+ end
1329
+
1330
+ def find_common_merge_node(starts, fwd, by_id)
1331
+ sets = starts.compact.map { reachable_merge_ids(_1, fwd, by_id) }
1332
+ return nil if sets.empty?
1333
+
1334
+ sets.reduce { |common, ids| common & ids }.first
1335
+ end
1336
+
1337
+ def reachable_merge_ids(start_id, fwd, by_id)
1338
+ result = []
1339
+ visited = {}
1340
+ queue = [start_id].compact
1341
+ while (id = queue.shift)
1342
+ next if visited[id]
1343
+ visited[id] = true
1344
+ obj = by_id[id]
1345
+ next unless obj
1346
+ result << id if obj["$Type"] == "Microflows$ExclusiveMerge"
1347
+ (fwd[id] || []).each { |e| queue << e[:to] }
1348
+ end
1349
+ result
1350
+ end
1351
+
1352
+ def action_dsl_line(obj, indent)
1353
+ action = obj["Action"] || {}
1354
+ pad = " " * indent
1355
+ case action["$Type"]
1356
+ when "Microflows$CreateObjectAction", "Microflows$CreateChangeAction"
1357
+ legacy = action["$Type"] == "Microflows$CreateChangeAction"
1358
+ members = member_specs(action[legacy ? "Items" : "Members"])
1359
+ has_associations = members.any? { !_1[:association].to_s.empty? }
1360
+ set_arg = members.empty? || has_associations ?
1361
+ "" : ", set: { #{members_dsl(members)} }"
1362
+ variable = legacy ? action["VariableName"] : action["OutputVariableName"]
1363
+ commit_a = action["Commit"] == "Yes" ? ", commit: true" : ""
1364
+ commit_a = ", commit: true, with_events: false" if action["Commit"] == "YesWithoutEvents"
1365
+ refresh_a = action["RefreshInClient"] == true ? ", refresh: true" : ""
1366
+ command = "#{pad}create_object #{ruby(action["Entity"])}, as: :#{variable}#{set_arg}#{commit_a}#{refresh_a}"
1367
+ has_associations ? member_block(command, members, indent) : command
1368
+ when "Microflows$ChangeObjectAction", "Microflows$ChangeAction"
1369
+ legacy = action["$Type"] == "Microflows$ChangeAction"
1370
+ members = member_specs(action[legacy ? "Items" : "Members"])
1371
+ variable = legacy ? action["ChangeVariableName"] : action["Variable"]
1372
+ commit_a = action["Commit"] == "Yes" ? ", commit: true" : ""
1373
+ commit_a = ", commit: true, with_events: false" if action["Commit"] == "YesWithoutEvents"
1374
+ refresh_a = action["RefreshInClient"] == true ? ", refresh: true" : ""
1375
+ has_associations = members.any? { !_1[:association].to_s.empty? }
1376
+ set_arg = members.empty? || has_associations ?
1377
+ "" : ", set: { #{members_dsl(members)} }"
1378
+ command = "#{pad}change_object :#{variable}#{set_arg}#{commit_a}#{refresh_a}"
1379
+ has_associations ? member_block(command, members, indent) : command
1380
+ when "Microflows$RetrieveAction"
1381
+ src = action["RetrieveSource"] || {}
1382
+ variable = action["ResultVariableName"] || action["ResultListName"]
1383
+ if src["$Type"] == "Microflows$AssociationRetrieveSource"
1384
+ "#{pad}retrieve_association :#{src["StartVariableName"]}, " \
1385
+ "association: #{ruby(src["AssociationId"])}, as: :#{variable}"
1386
+ else
1387
+ xpath = (src["XpathConstraint"] || src["XPath"]).to_s
1388
+ args = [ruby(src["Entity"]), "as: :#{variable}"]
1389
+ args << "xpath: #{ruby(xpath)}" unless xpath.empty?
1390
+ range = src["Range"] || {}
1391
+ if range["$Type"] == "Microflows$CustomRange"
1392
+ limit = range["LimitExpression"].to_s
1393
+ args << "limit: #{ruby(limit)}" unless limit.empty?
1394
+ elsif range["SingleObject"] == true
1395
+ args << "single: true"
1396
+ end
1397
+ sortings = bson_items(src.dig("NewSortings", "Sortings") || src["SortItems"])
1398
+ unless sortings.empty?
1399
+ rendered = sortings.map do |sorting|
1400
+ attribute = sorting["AttributePath"] ||
1401
+ sorting.dig("AttributeRef", "Attribute")
1402
+ "[#{ruby(attribute)}, :#{underscore(sorting["SortOrder"])}]"
1403
+ end
1404
+ args << "sort: [#{rendered.join(', ')}]"
1405
+ end
1406
+ "#{pad}retrieve_objects #{args.join(', ')}"
1407
+ end
1408
+ when "Microflows$CommitObjectsAction", "Microflows$CommitAction"
1409
+ legacy = action["$Type"] == "Microflows$CommitAction"
1410
+ variable = legacy ? action["CommitVariableName"] : action["Variable"]
1411
+ with_events = legacy ? action.fetch("WithEvents", true) : action["CommitWithoutEvents"] != true
1412
+ events_a = with_events ? "" : ", with_events: false"
1413
+ refresh_a = action["RefreshInClient"] == true ? ", refresh: true" : ""
1414
+ "#{pad}commit :#{variable}#{events_a}#{refresh_a}"
1415
+ when "Microflows$DeleteAction"
1416
+ variable = action["DeleteVariableName"] || action["DeleteVariable"]
1417
+ refresh_a = action["RefreshInClient"] == true ? ", refresh: true" : ""
1418
+ "#{pad}delete :#{variable}#{refresh_a}"
1419
+ when "Microflows$MicroflowCallAction"
1420
+ call = action["MicroflowCall"] || {}
1421
+ out = if action["UseReturnVariable"] == true
1422
+ (action["ResultVariableName"] || action["OutputVariableName"]).to_s
1423
+ else
1424
+ ""
1425
+ end
1426
+ as_a = out.empty? ? "" : ", as: :#{out}"
1427
+ stored_result = (action["ResultVariableName"] || action["OutputVariableName"]).to_s
1428
+ result_a = if out.empty? && !stored_result.empty?
1429
+ ", result_name: #{symbol(stored_result)}, use_return: false"
1430
+ elsif action["UseReturnVariable"] == true && out.empty?
1431
+ ", use_return: true"
1432
+ else
1433
+ ""
1434
+ end
1435
+ params = bson_items(call["ParameterMappings"]).map { [_1["Parameter"], _1["Argument"]] }
1436
+ pass_a = params.empty? ? "" : ", pass: #{pass_source(params)}"
1437
+ "#{pad}call_microflow #{ruby(call["Microflow"])}#{as_a}#{pass_a}#{result_a}"
1438
+ when "Microflows$CreateVariableAction"
1439
+ type = action.dig("VariableType", "$Type").to_s
1440
+ type = type.delete_prefix("DataTypes$").delete_suffix("Type")
1441
+ value = action["InitialValue"]
1442
+ value_a = value.nil? || value.to_s.empty? ? "" : ", value: #{ruby_val(value)}"
1443
+ type_a = type.empty? ? "" : ", type: #{symbol(type)}"
1444
+ "#{pad}create_variable :#{action["VariableName"]}#{type_a}#{value_a}"
1445
+ when "Microflows$ChangeVariableAction"
1446
+ "#{pad}change_variable :#{action["ChangeVariableName"]}, to: #{ruby_val(action["Value"])}"
1447
+ when "Microflows$ShowMessageAction"
1448
+ template = action["Template"] || {}
1449
+ translations = translated_items(template["Text"]).to_h do |translation|
1450
+ [translation["LanguageCode"].to_s, translation["Text"].to_s]
1451
+ end
1452
+ parameters = bson_items(template["Parameters"]).map { _1["Expression"] }
1453
+ text = translations["en_US"] || translations.values.first.to_s
1454
+ type_a = action["Type"].to_s.empty? ? "" : ", type: :#{underscore(action["Type"])}"
1455
+ blocking_a = action["Blocking"] == true ? ", blocking: true" : ""
1456
+ translations_a = translations.size > 1 ? ", translations: #{ruby(translations)}" : ""
1457
+ parameters_a = parameters.empty? ? "" : ", parameters: #{ruby(parameters)}"
1458
+ "#{pad}show_message #{ruby(text)}#{type_a}#{blocking_a}#{translations_a}#{parameters_a}"
1459
+ when "Microflows$LogMessageAction"
1460
+ template = action["MessageTemplate"] || {}
1461
+ parameters = bson_items(template["Parameters"]).map { _1["Expression"] }
1462
+ level_a = action["Level"].to_s.empty? ? "" : ", level: :#{underscore(action["Level"])}"
1463
+ node_a = action["Node"].to_s.empty? ? "" : ", node: #{ruby(action["Node"])}"
1464
+ stack_a = action["IncludeLatestStackTrace"] == true ? ", include_stack: true" : ""
1465
+ parameters_a = parameters.empty? ? "" : ", parameters: #{ruby(parameters)}"
1466
+ "#{pad}log_message #{ruby(template["Text"].to_s)}#{level_a}#{node_a}#{stack_a}#{parameters_a}"
1467
+ when "Microflows$ShowFormAction"
1468
+ settings = action["FormSettings"] || {}
1469
+ args = [ruby(settings["Form"])]
1470
+ variable = action["FormObjectVariable"].to_s
1471
+ args << "object: :#{variable}" unless variable.empty?
1472
+ location = settings["Location"].to_s
1473
+ args << "location: :#{underscore(location)}" unless location.empty?
1474
+ mappings = bson_items(settings["ParameterMappings"]).map do |mapping|
1475
+ "#{ruby(mapping["Parameter"])} => #{ruby_val(mapping["Argument"])}"
1476
+ end
1477
+ args << "pass: { #{mappings.join(', ')} }" unless mappings.empty?
1478
+ close_pages = action["NumberOfPagesToClose"].to_s
1479
+ args << "close_pages: #{close_pages.to_i}" unless close_pages.empty?
1480
+ title = settings["FormTitle"] || settings["TitleOverride"]
1481
+ translations = translated_items(title&.fetch("Text", title)).to_h do |translation|
1482
+ [translation["LanguageCode"].to_s, translation["Text"].to_s]
1483
+ end
1484
+ args << "title: #{ruby(translations)}" unless translations.empty?
1485
+ "#{pad}show_page #{args.join(', ')}"
1486
+ when "Microflows$CloseFormAction"
1487
+ count = action["NumberOfPagesToClose"].to_s
1488
+ count.empty? ? "#{pad}close_page" : "#{pad}close_page count: #{count.to_i}"
1489
+ when "Microflows$JavaActionCallAction"
1490
+ call_action_line(
1491
+ pad, "call_java", action["JavaAction"], action["ResultVariableName"],
1492
+ bson_items(action["ParameterMappings"]).map {
1493
+ [_1["Parameter"], code_action_parameter_value(_1["Value"])]
1494
+ },
1495
+ action["UseReturnVariable"]
1496
+ )
1497
+ when "Microflows$JavaScriptActionCallAction"
1498
+ call_action_line(
1499
+ pad, "call_javascript", action["JavaScriptAction"], action["OutputVariableName"],
1500
+ bson_items(action["ParameterMappings"]).map {
1501
+ [_1["Parameter"], code_action_parameter_value(_1["ParameterValue"])]
1502
+ },
1503
+ action["UseReturnVariable"]
1504
+ )
1505
+ when "Microflows$NanoflowCallAction"
1506
+ call = action["NanoflowCall"] || {}
1507
+ call_action_line(
1508
+ pad, "call_nanoflow", call["Nanoflow"], action["OutputVariableName"],
1509
+ bson_items(call["ParameterMappings"]).map { [_1["Parameter"], _1["Argument"]] },
1510
+ action["UseReturnVariable"]
1511
+ )
1512
+ when "Microflows$AppServiceCallAction"
1513
+ call_action_line(
1514
+ pad, "call_app_service", action["AppServiceAction"], action["ResultVariableName"],
1515
+ bson_items(action["ParameterMappings"]).map { [_1["Parameter"], _1["Argument"]] },
1516
+ action["UseReturnVariable"]
1517
+ )
1518
+ when "Microflows$AggregateAction"
1519
+ args = [
1520
+ ":#{action["AggregateVariableName"]}",
1521
+ "function: :#{underscore(action["AggregateFunction"])}",
1522
+ "as: :#{action["VariableName"]}"
1523
+ ]
1524
+ args << "attribute: #{ruby(action["Attribute"])}" unless action["Attribute"].to_s.empty?
1525
+ "#{pad}aggregate #{args.join(', ')}"
1526
+ when "Microflows$RollbackAction"
1527
+ refresh = action["RefreshInClient"] == true ? ", refresh: true" : ""
1528
+ "#{pad}rollback :#{action["RollbackVariableName"]}#{refresh}"
1529
+ when "Microflows$CastAction"
1530
+ "#{pad}cast :#{action["VariableName"]}"
1531
+ when "Microflows$CreateListAction"
1532
+ "#{pad}create_list #{ruby(action["Entity"])}, as: :#{action["VariableName"]}"
1533
+ when "Microflows$ListOperationsAction"
1534
+ operation = action["NewOperation"] || {}
1535
+ op = operation["$Type"].to_s.delete_prefix("Microflows$")
1536
+ args = [":#{underscore(op)}", ":#{operation['ListName']}"]
1537
+ second = operation["SecondListOrObjectName"].to_s
1538
+ expression = operation["Expression"]
1539
+ args << "with: :#{second}" unless second.empty?
1540
+ args << "expression: #{ruby(expression)}" unless expression.nil?
1541
+ args << "as: :#{action['ResultVariableName']}"
1542
+ "#{pad}list_operation #{args.join(', ')}"
1543
+ when "Microflows$ChangeListAction"
1544
+ "#{pad}change_list :#{action["ChangeVariableName"]}, " \
1545
+ "action: :#{underscore(action["Type"])}, value: #{ruby_val(action["Value"])}"
1546
+ when "Microflows$ValidationFeedbackAction"
1547
+ template = action["FeedbackTemplate"] || {}
1548
+ translations = translated_items(template["Text"]).to_h do |translation|
1549
+ [translation["LanguageCode"].to_s, translation["Text"].to_s]
1550
+ end
1551
+ parameters = bson_items(template["Parameters"]).map { _1["Expression"] }
1552
+ args = [":#{action["ValidationVariableName"]}"]
1553
+ args << "attribute: #{ruby(action["Attribute"])}" unless action["Attribute"].to_s.empty?
1554
+ args << "association: #{ruby(action["Association"])}" unless action["Association"].to_s.empty?
1555
+ args << "translations: #{ruby(translations)}"
1556
+ args << "parameters: #{ruby(parameters)}" unless parameters.empty?
1557
+ error = underscore(action["ErrorHandlingType"])
1558
+ args << "error: :#{error}" unless error == "rollback"
1559
+ "#{pad}validation_feedback #{args.join(', ')}"
1560
+ when "Microflows$RestCallAction"
1561
+ rest_call_line(pad, action)
1562
+ end
1563
+ end
1564
+
1565
+ def rest_call_line(pad, action)
1566
+ http = action["HttpConfiguration"] || {}
1567
+ location = http["CustomLocationTemplate"] || {}
1568
+ headers = bson_items(http["HttpHeaderEntries"]).to_h do |header|
1569
+ [header["Key"].to_s, header["Value"].to_s]
1570
+ end
1571
+ request = action["RequestHandling"] || {}
1572
+ result = action["ResultHandling"] || {}
1573
+ import_call = result["ImportMappingCall"] || {}
1574
+ args = [
1575
+ "method: :#{underscore(http["HttpMethod"])}",
1576
+ "location: #{ruby(location["Text"].to_s)}"
1577
+ ]
1578
+ parameters = bson_items(location["Parameters"]).map { _1["Expression"] }
1579
+ args << "location_parameters: #{ruby(parameters)}" unless parameters.empty?
1580
+ args << "headers: #{ruby(headers)}" unless headers.empty?
1581
+ args << "request_mapping: #{ruby(request["MappingId"])}" if request["MappingId"]
1582
+ args << "request_variable: :#{request["MappingVariableName"]}" if request["MappingVariableName"]
1583
+ args << "result_mapping: #{ruby(import_call["ReturnValueMapping"])}" if import_call["ReturnValueMapping"]
1584
+ args << "as: :#{result["ResultVariableName"]}" unless result["ResultVariableName"].to_s.empty?
1585
+ args << "result_entity: #{ruby(result.dig("VariableType", "Entity"))}" if result.dig("VariableType", "Entity")
1586
+ args << "timeout: #{ruby(action["TimeOutExpression"])}" if action["UseRequestTimeOut"] == true
1587
+ args << "commit: :#{underscore(import_call["Commit"])}" unless import_call["Commit"].to_s.empty?
1588
+ args << "error_result: :#{underscore(action["ErrorResultHandlingType"])}"
1589
+ args << "error: :#{underscore(action["ErrorHandlingType"])}"
1590
+ "#{pad}call_rest #{args.join(', ')}"
1591
+ end
1592
+
1593
+ def call_action_line(pad, method, target, output, mappings, use_return)
1594
+ args = [ruby(target)]
1595
+ if use_return == true && !output.to_s.empty?
1596
+ args << "as: :#{output}"
1597
+ elsif !output.to_s.empty?
1598
+ args << "result_name: #{symbol(output)}"
1599
+ args << "use_return: false"
1600
+ elsif use_return == true
1601
+ args << "use_return: true"
1602
+ end
1603
+ unless mappings.empty?
1604
+ args << "pass: #{pass_source(mappings)}"
1605
+ end
1606
+ "#{pad}#{method} #{args.join(', ')}"
1607
+ end
1608
+
1609
+ def pass_source(mappings)
1610
+ duplicate = mappings.map { _1.first.to_s }.tally.values.any? { _1 > 1 }
1611
+ rendered = mappings.map do |parameter, value|
1612
+ key = ruby(parameter)
1613
+ val = ruby_val(value)
1614
+ duplicate ? "[#{key}, #{val}]" : "#{key} => #{val}"
1615
+ end
1616
+ duplicate ? "[#{rendered.join(', ')}]" : "{ #{rendered.join(', ')} }"
1617
+ end
1618
+
1619
+ def code_action_parameter_value(value)
1620
+ value ||= {}
1621
+ type = value["$Type"].to_s
1622
+ return value["Argument"] if type.include?("$Basic")
1623
+
1624
+ field, kind = case type
1625
+ when /EntityType/ then ["Entity", :entity]
1626
+ when /MicroflowJava/ then ["Microflow", :microflow]
1627
+ when /ImportMapping/ then ["ImportMapping", :import_mapping]
1628
+ when /ExportMapping/ then ["ExportMapping", :export_mapping]
1629
+ else
1630
+ [nil, :native]
1631
+ end
1632
+ { kind: kind, value: field ? value[field] : value }
1633
+ end
1634
+
1635
+ def translated_text(text_doc)
1636
+ return "" unless text_doc
1637
+
1638
+ translations = text_doc["Translations"] || text_doc["Items"]
1639
+ bson_items(translations).first&.fetch("Text", "") || ""
1640
+ end
1641
+
1642
+ def translated_items(text_doc)
1643
+ return [] unless text_doc
1644
+
1645
+ bson_items(text_doc["Translations"] || text_doc["Items"])
1646
+ end
1647
+
1648
+ def member_specs(members_bson)
1649
+ bson_items(members_bson).filter_map do |m|
1650
+ attr = m["Attribute"].to_s
1651
+ association = m["Association"].to_s
1652
+ raw_value = m["Value"]
1653
+ val = raw_value.is_a?(Hash) ? raw_value["Value"].to_s : raw_value.to_s
1654
+ next if attr.empty? && association.empty?
1655
+
1656
+ {
1657
+ attribute: attr, association: association, value: val,
1658
+ operation: m.fetch("Type", "Set")
1659
+ }
1660
+ end
1661
+ end
1662
+
1663
+ def members_dsl(members)
1664
+ members.map { "#{symbol(_1[:attribute])} => #{ruby_val(_1[:value])}" }.join(", ")
1665
+ end
1666
+
1667
+ def member_block(command, members, indent)
1668
+ pad = " " * (indent + 2)
1669
+ lines = ["#{command} do"]
1670
+ members.each do |member|
1671
+ if member[:association].to_s.empty?
1672
+ lines << "#{pad}set #{symbol(member[:attribute])}, to: #{ruby_val(member[:value])}"
1673
+ else
1674
+ operation = underscore(member[:operation])
1675
+ operation_a = operation == "set" ? "" : ", operation: :#{operation}"
1676
+ lines << "#{pad}set_association #{ruby(member[:association])}, " \
1677
+ "to: #{ruby_val(member[:value])}#{operation_a}"
1678
+ end
1679
+ end
1680
+ lines << "#{' ' * indent}end"
1681
+ lines.join("\n")
1682
+ end
1683
+
1684
+ def ruby_val(val)
1685
+ return "nil" if val.nil? || val.to_s.empty?
1686
+ return ruby(val) if val.is_a?(Hash) || val.is_a?(Array)
1687
+ s = val.to_s
1688
+ return s if s.match?(/\A-?\d+(?:\.\d+)?\z/) || %w[true false nil].include?(s)
1689
+
1690
+ ruby(s)
1691
+ end
1692
+
1693
+ def bson_items(bson)
1694
+ IO::BsonCodec.parse_array(bson)[:items]
1695
+ rescue StandardError
1696
+ []
1697
+ end
1698
+
1699
+ def ruby(value)
1700
+ value.inspect
1701
+ end
1702
+
1703
+ def architecture_module(name)
1704
+ @architecture&.fetch(:modules, [])&.find { _1[:name] == name }
1705
+ end
1706
+
1707
+ def symbol(value)
1708
+ value.to_s.to_sym.inspect
1709
+ end
1710
+
1711
+ def write(path, contents)
1712
+ FileUtils.mkdir_p(File.dirname(path))
1713
+ File.write(path, contents)
1714
+ end
1715
+ end
1716
+ end