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,1636 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "base64"
6
+
7
+ module Mxrb
8
+ module Dsl
9
+ # Shared widget-building methods for PageBuilder and ContainerBuilder.
10
+ # Includers must implement private `_widget_list` returning the target array.
11
+ module WidgetDsl
12
+ def text_box(name, attribute: nil, caption: nil, &block)
13
+ _add_widget(:text_box, name, attribute: attribute, caption: caption, &block)
14
+ end
15
+
16
+ def number_input(name, attribute: nil, caption: nil, &block)
17
+ _add_widget(:number_input, name, attribute: attribute, caption: caption, &block)
18
+ end
19
+
20
+ def text_area(name, attribute: nil, caption: nil, lines: 5, &block)
21
+ _add_widget(
22
+ :text_area, name, attribute: attribute, caption: caption, lines: lines, &block
23
+ )
24
+ end
25
+
26
+ def check_box(name, attribute: nil, caption: nil, &block)
27
+ _add_widget(:check_box, name, attribute: attribute, caption: caption, &block)
28
+ end
29
+
30
+ def date_picker(name, attribute: nil, caption: nil, &block)
31
+ _add_widget(:date_picker, name, attribute: attribute, caption: caption, &block)
32
+ end
33
+
34
+ def reference_selector(name, attribute: nil, caption: nil, display_attribute: nil, &block)
35
+ _add_widget(
36
+ :reference_selector, name, attribute: attribute, caption: caption,
37
+ display_attribute: display_attribute, &block
38
+ )
39
+ end
40
+
41
+ def text(name, caption: nil, &block)
42
+ _add_widget(:text, name, caption: caption || name.to_s, &block)
43
+ end
44
+
45
+ def data_grid(name, entity: nil, &block)
46
+ _add_widget(:data_grid, name, entity: entity, &block)
47
+ end
48
+
49
+ def tab_control(name, &block)
50
+ _add_widget(:tab_control, name, &block)
51
+ end
52
+
53
+ def button(name, caption: nil, &block)
54
+ _add_widget(:button, name, caption: caption || name.to_s, &block)
55
+ end
56
+
57
+ def drop_down(name, attribute: nil, caption: nil)
58
+ _widget_list << {
59
+ type: :drop_down, name: name.to_s,
60
+ options: { attribute: attribute&.to_s, caption: caption }.compact,
61
+ events: []
62
+ }
63
+ end
64
+
65
+ def snippet(name, from: nil)
66
+ _widget_list << {
67
+ type: :snippet, name: name.to_s,
68
+ options: { snippet: (from || name).to_s },
69
+ events: []
70
+ }
71
+ end
72
+
73
+ def container(name, class_name: nil, &block)
74
+ cb = ContainerBuilder.new(name, class_name: class_name)
75
+ cb.instance_eval(&block) if block
76
+ _widget_list << cb.to_h
77
+ end
78
+
79
+ def pluggable_widget(name, widget_id:, widget_name: nil, properties: {}, class_name: nil)
80
+ _widget_list << {
81
+ type: :pluggable_widget, name: name.to_s,
82
+ options: {
83
+ widget_id: widget_id.to_s, widget_name: (widget_name || name).to_s,
84
+ properties: properties, class: class_name
85
+ }.compact,
86
+ events: []
87
+ }
88
+ end
89
+
90
+ def native_widget(name, type:, deep_structure:)
91
+ raise ArgumentError, "deep_structure requires a Hash" unless deep_structure.is_a?(Hash)
92
+
93
+ _widget_list << {
94
+ type: :native_widget, name: name.to_s,
95
+ options: { native_type: type.to_s, deep_structure: deep_structure }, events: []
96
+ }
97
+ end
98
+
99
+ # Exported native widget payloads can contain BSON identifiers at any
100
+ # nesting level. Keep the reconstruction helper on the shared widget DSL
101
+ # so pages, containers, and tab pages all evaluate the same export.
102
+ def bson_binary(base64, subtype: :generic)
103
+ BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
104
+ end
105
+
106
+ private
107
+
108
+ def _add_widget(type, name, **options, &block)
109
+ builder = WidgetBuilder.new(type, name, **options)
110
+ builder.instance_eval(&block) if block
111
+ _widget_list << builder.to_h
112
+ end
113
+ end
114
+
115
+ # Builds sub-widgets inside a data_grid column sub-items or inside a container.
116
+ class WidgetBuilder
117
+ def initialize(type, name, **options)
118
+ @type = type
119
+ @name = name.to_s
120
+ @options = options
121
+ @events = []
122
+ @columns = []
123
+ @tabs = []
124
+ @search_bar = nil
125
+ @toolbar = nil
126
+ end
127
+
128
+ def column(name, attribute: nil, caption: nil)
129
+ @columns << { name: name.to_s, attribute: attribute&.to_s, caption: caption }.compact
130
+ end
131
+
132
+ def tab_page(name, caption: nil, &block)
133
+ tab = TabPageBuilder.new(name, caption: caption)
134
+ tab.instance_eval(&block) if block
135
+ @tabs << tab.to_h
136
+ end
137
+
138
+ def search_bar(&block)
139
+ sb = SearchBarBuilder.new
140
+ sb.instance_eval(&block) if block
141
+ @search_bar = sb.to_h
142
+ end
143
+
144
+ def toolbar(&block)
145
+ tb = ToolbarBuilder.new
146
+ tb.instance_eval(&block) if block
147
+ @toolbar = tb.to_h
148
+ end
149
+
150
+ %i[on_change on_click on_enter on_leave].each do |event|
151
+ define_method(event) do |microflow: nil, nanoflow: nil, action: nil|
152
+ choices = { microflow: microflow, nanoflow: nanoflow, action: action }.compact
153
+ raise ArgumentError, "#{event} requires exactly one handler" unless choices.size == 1
154
+ @events << { event: event, kind: choices.keys.first, handler: choices.values.first.to_s }
155
+ end
156
+ end
157
+
158
+ def to_h
159
+ options = @options.dup
160
+ options[:columns] = @columns unless @columns.empty?
161
+ options[:tabs] = @tabs unless @tabs.empty?
162
+ options[:search_bar] = @search_bar if @search_bar
163
+ options[:toolbar] = @toolbar if @toolbar
164
+ { type: @type, name: @name, options: options, events: @events }
165
+ end
166
+ end
167
+
168
+ class TabPageBuilder
169
+ include WidgetDsl
170
+
171
+ def initialize(name, caption: nil)
172
+ @name = name.to_s
173
+ @caption = caption || name.to_s
174
+ @widgets = []
175
+ end
176
+
177
+ def to_h = { name: @name, caption: @caption, widgets: @widgets }
178
+
179
+ private
180
+
181
+ def _widget_list = @widgets
182
+ end
183
+
184
+ class SearchBarBuilder
185
+ def initialize
186
+ @fields = []
187
+ end
188
+
189
+ def search_field(attribute, caption: nil)
190
+ @fields << { attribute: attribute.to_s, caption: caption }
191
+ end
192
+
193
+ def to_h
194
+ { fields: @fields }
195
+ end
196
+ end
197
+
198
+ class ToolbarBuilder
199
+ def initialize
200
+ @buttons = []
201
+ end
202
+
203
+ def new_button(caption: "New")
204
+ @buttons << { type: :new, caption: caption }
205
+ end
206
+
207
+ def delete_button(caption: "Delete")
208
+ @buttons << { type: :delete, caption: caption }
209
+ end
210
+
211
+ def search_button(caption: "Search")
212
+ @buttons << { type: :search, caption: caption }
213
+ end
214
+
215
+ def export_button(caption: "Export")
216
+ @buttons << { type: :export, caption: caption }
217
+ end
218
+
219
+ def to_h
220
+ { buttons: @buttons }
221
+ end
222
+ end
223
+
224
+ # Builds nested widgets inside a container widget.
225
+ class ContainerBuilder
226
+ include WidgetDsl
227
+
228
+ def initialize(name, class_name: nil)
229
+ @name = name.to_s
230
+ @class_name = class_name&.to_s
231
+ @children = []
232
+ end
233
+
234
+ def to_h
235
+ opts = {}
236
+ opts[:class] = @class_name if @class_name && !@class_name.empty?
237
+ { type: :container, name: @name, options: opts, children: @children, events: [] }
238
+ end
239
+
240
+ private
241
+
242
+ def _widget_list = @children
243
+ end
244
+
245
+ # Entry point for the project-definition DSL.
246
+ # Used by Mxrb.define { ... }
247
+ module ConnectorDeclarations
248
+ def connector(protocol, version: nil, marketplace_id: nil)
249
+ connector_requests << Protocols.request(protocol, version:, marketplace_id:)
250
+ end
251
+
252
+ def connector_plans(adapter: nil)
253
+ connector_requests.map do |request|
254
+ Protocols.plan(
255
+ request.protocol, mendix_version: @mendix_version, version: request.version,
256
+ marketplace_id: request.marketplace_id, adapter:
257
+ )
258
+ end
259
+ end
260
+
261
+ def validate_connector_requests!
262
+ return if connector_requests.empty?
263
+
264
+ raise MarketplaceError,
265
+ 'connector declarations are preview-only; resolve and apply connector_plans ' \
266
+ 'to an existing MPR with the official Marketplace adapter'
267
+ end
268
+
269
+ def connector_requests = (@connector_requests ||= [])
270
+ end
271
+
272
+ class Builder
273
+ include ConnectorDeclarations
274
+
275
+ def initialize(path)
276
+ @path = path
277
+ @mendix_version = "10.18.0"
278
+ @modules = []
279
+ @security = nil
280
+ @navigation = nil
281
+ @design_system = nil
282
+ @native_units_path = nil
283
+ @native_unit_overrides = []
284
+ @project_assets = nil
285
+ end
286
+
287
+ def mendix_version(v)
288
+ @mendix_version = v
289
+ end
290
+
291
+ def module(name, &block)
292
+ mod = ModuleBuilder.new(name)
293
+ mod.instance_eval(&block) if block
294
+ @modules << mod
295
+ end
296
+
297
+ def security(&block)
298
+ builder = SecurityBuilder.new
299
+ builder.instance_eval(&block) if block
300
+ @security = builder.to_h
301
+ end
302
+
303
+ def navigation(&block)
304
+ builder = NavigationBuilder.new
305
+ builder.instance_eval(&block) if block
306
+ @navigation = builder.to_h
307
+ end
308
+
309
+ def design_system(&block)
310
+ builder = DesignSystemBuilder.new
311
+ builder.instance_eval(&block) if block
312
+ @design_system = builder.to_h
313
+ end
314
+
315
+ def native_units(path)
316
+ @native_units_path = path
317
+ end
318
+
319
+ def project_assets(manifest, root:)
320
+ @project_assets = {
321
+ manifest: File.expand_path(manifest),
322
+ root: File.expand_path(root)
323
+ }
324
+ end
325
+
326
+ def native_unit(unit_id, container_id:, containment:, deep_structure:, module_name: nil)
327
+ raise ArgumentError, "deep_structure requires a Hash" unless deep_structure.is_a?(Hash)
328
+
329
+ @native_unit_overrides << {
330
+ unit_id: unit_id.to_s,
331
+ container_id: container_id.to_s,
332
+ containment: containment.to_s,
333
+ module: module_name&.to_s,
334
+ doc: deep_structure
335
+ }
336
+ end
337
+
338
+ def bson_binary(base64, subtype: :generic)
339
+ BSON::Binary.new(Base64.strict_decode64(base64), subtype)
340
+ end
341
+
342
+ def evaluate(path)
343
+ instance_eval(File.read(path), path, 1)
344
+ end
345
+
346
+ def evaluate_dir(dir)
347
+ return unless File.directory?(dir)
348
+
349
+ Dir[File.join(dir, '*.rb')].sort.each { |path| evaluate(path) }
350
+ end
351
+
352
+ def build!
353
+ validate_connector_requests!
354
+ validate!
355
+ Writer.new(@path, definition).write!
356
+ self
357
+ end
358
+
359
+ def graph
360
+ Architecture::Graph.new(definition)
361
+ end
362
+
363
+ def validate!
364
+ Architecture::Validator.new(graph).validate!
365
+ end
366
+
367
+ def definition
368
+ {
369
+ version: @mendix_version,
370
+ modules: @modules.map(&:to_h),
371
+ security: @security,
372
+ navigation: @navigation,
373
+ design_system: @design_system,
374
+ connectors: connector_requests.map(&:to_h),
375
+ project_assets: @project_assets,
376
+ native_units_path: @native_units_path,
377
+ native_unit_overrides: @native_unit_overrides
378
+ }
379
+ end
380
+ end
381
+
382
+ class SecurityBuilder
383
+ def initialize
384
+ @user_roles = []
385
+ @security_level = nil
386
+ @admin_user_role = nil
387
+ @demo_users_enabled = nil
388
+ @guest_access_enabled = nil
389
+ @guest_user_role = nil
390
+ @sign_in_microflow = nil
391
+ @password_policy = nil
392
+ end
393
+
394
+ # Mendix stores the project security mode as an enum-like string such as
395
+ # "CheckNothing" or "CheckEverything". Keep the native value explicit so
396
+ # export/import does not silently weaken project security.
397
+ def security_level(value)
398
+ @security_level = value.to_s
399
+ end
400
+
401
+ def user_role(name, module_roles: [], admin: false)
402
+ @user_roles << {
403
+ name: name.to_s,
404
+ module_roles: Array(module_roles).map(&:to_s),
405
+ admin: admin
406
+ }
407
+ end
408
+
409
+ def admin_user_role(name)
410
+ @admin_user_role = name.to_s
411
+ end
412
+
413
+ def demo_users(enabled = true)
414
+ @demo_users_enabled = enabled == true
415
+ end
416
+
417
+ def guest_access(enabled = true, role: nil)
418
+ @guest_access_enabled = enabled == true
419
+ @guest_user_role = role&.to_s
420
+ end
421
+
422
+ def sign_in_microflow(name)
423
+ @sign_in_microflow = name&.to_s
424
+ end
425
+
426
+ def password_policy(**options)
427
+ @password_policy = options.transform_keys(&:to_sym)
428
+ end
429
+
430
+ def to_h
431
+ {
432
+ user_roles: @user_roles,
433
+ security_level: @security_level,
434
+ admin_user_role: @admin_user_role,
435
+ demo_users_enabled: @demo_users_enabled,
436
+ guest_access_enabled: @guest_access_enabled,
437
+ guest_user_role: @guest_user_role,
438
+ sign_in_microflow: @sign_in_microflow,
439
+ password_policy: @password_policy
440
+ }
441
+ end
442
+ end
443
+
444
+ class NavigationBuilder
445
+ def initialize
446
+ @profiles = []
447
+ end
448
+
449
+ def profile(name, home_page: nil, home_microflow: nil, sign_in_page: nil,
450
+ menu: nil, role_homes: {}, offline: false, kind: nil,
451
+ app_title: nil, app_icon: nil, &block)
452
+ profile = NavigationProfileBuilder.new(
453
+ name, home_page:, home_microflow:, sign_in_page:, menu:, role_homes:,
454
+ offline:, kind:, app_title:, app_icon:
455
+ )
456
+ profile.instance_eval(&block) if block
457
+ @profiles << profile.to_h
458
+ end
459
+
460
+ def to_h
461
+ { profiles: @profiles }
462
+ end
463
+ end
464
+
465
+ class NavigationProfileBuilder
466
+ def initialize(name, **options)
467
+ @name = name.to_s
468
+ @home_page = optional_string(options, :home_page)
469
+ @home_microflow = optional_string(options, :home_microflow)
470
+ @sign_in_page = optional_string(options, :sign_in_page)
471
+ @menu = optional_string(options, :menu)
472
+ @offline = options[:offline] == true
473
+ @kind = optional_string(options, :kind)
474
+ @app_icon = optional_string(options, :app_icon)
475
+ @app_title = normalize_translations(options[:app_title])
476
+ @role_homes = options.fetch(:role_homes, {}).to_h.transform_keys(&:to_s)
477
+ .transform_values(&:to_s)
478
+ @role_home_details = []
479
+ @items = []
480
+ end
481
+
482
+ def title(locale, text)
483
+ @app_title[locale.to_s] = text.to_s
484
+ end
485
+
486
+ def home_for(role, page: nil, microflow: nil)
487
+ @role_home_details << {
488
+ role: role.to_s, page: page&.to_s, microflow: microflow&.to_s
489
+ }.compact
490
+ end
491
+
492
+ def item(caption, page: nil, microflow: nil, icon: nil, translations: {}, &block)
493
+ builder = NavigationMenuItemBuilder.new(
494
+ caption, page:, microflow:, icon:, translations:
495
+ )
496
+ builder.instance_eval(&block) if block
497
+ @items << builder.to_h
498
+ end
499
+
500
+ def evaluate(path)
501
+ instance_eval(File.read(path), path, 1)
502
+ end
503
+
504
+ def evaluate_dir(dir)
505
+ return unless File.directory?(dir)
506
+
507
+ Dir[File.join(dir, '*.rb')].sort.each { |path| evaluate(path) }
508
+ end
509
+
510
+ def to_h
511
+ {
512
+ name: @name, home_page: @home_page, home_microflow: @home_microflow,
513
+ sign_in_page: @sign_in_page, menu: @menu, role_homes: @role_homes,
514
+ role_home_details: @role_home_details, offline: @offline, kind: @kind,
515
+ app_title: @app_title, app_icon: @app_icon, items: @items
516
+ }
517
+ end
518
+
519
+ private
520
+
521
+ def optional_string(options, key)
522
+ options[key]&.to_s
523
+ end
524
+
525
+ def normalize_translations(value)
526
+ return {} if value.nil?
527
+ return value.to_h.transform_keys(&:to_s).transform_values(&:to_s) if value.respond_to?(:to_h)
528
+
529
+ { "en_US" => value.to_s }
530
+ end
531
+ end
532
+
533
+ class NavigationMenuItemBuilder
534
+ def initialize(caption, page: nil, microflow: nil, icon: nil, translations: {})
535
+ @caption = normalize_caption(caption)
536
+ @caption.merge!(translations.to_h.transform_keys(&:to_s).transform_values(&:to_s))
537
+ @page = page&.to_s
538
+ @microflow = microflow&.to_s
539
+ @icon = icon
540
+ @items = []
541
+ end
542
+
543
+ def item(caption, page: nil, microflow: nil, icon: nil, translations: {}, &block)
544
+ child = self.class.new(caption, page:, microflow:, icon:, translations:)
545
+ child.instance_eval(&block) if block
546
+ @items << child.to_h
547
+ end
548
+
549
+ def to_h
550
+ {
551
+ caption: @caption, page: @page, microflow: @microflow,
552
+ icon: @icon, items: @items
553
+ }
554
+ end
555
+
556
+ private
557
+
558
+ def normalize_caption(caption)
559
+ return { "en_US" => caption.to_s } unless caption.respond_to?(:to_h)
560
+
561
+ caption.to_h.transform_keys(&:to_s).transform_values(&:to_s)
562
+ end
563
+ end
564
+
565
+ class DesignSystemBuilder
566
+ TOKEN_KINDS = %i[color spacing radius typography breakpoint].freeze
567
+
568
+ def initialize
569
+ @tokens = []
570
+ @themes = []
571
+ @layouts = []
572
+ @components = []
573
+ @accessibility = []
574
+ @contrast_pairs = []
575
+ @forbid_literal_colors = false
576
+ end
577
+
578
+ TOKEN_KINDS.each do |kind|
579
+ define_method(kind) do |name, value: nil|
580
+ @tokens << { kind:, name: name.to_s, value: value&.to_s }
581
+ end
582
+ end
583
+
584
+ def layout(name)
585
+ @layouts << name.to_s
586
+ end
587
+
588
+ def component(name)
589
+ @components << name.to_s
590
+ end
591
+
592
+ def accessibility(rule)
593
+ @accessibility << rule.to_s
594
+ end
595
+
596
+ def theme(name, inherits: nil, &block)
597
+ builder = DesignThemeBuilder.new(name, inherits:)
598
+ builder.instance_eval(&block) if block
599
+ @themes << builder.to_h
600
+ end
601
+
602
+ def contrast(foreground:, background:, level: :aa)
603
+ @contrast_pairs << {
604
+ foreground: foreground.to_s, background: background.to_s, level: level.to_s
605
+ }
606
+ end
607
+
608
+ def forbid_literal_colors(value = true)
609
+ @forbid_literal_colors = value == true
610
+ end
611
+
612
+ def to_h
613
+ {
614
+ tokens: @tokens, layouts: @layouts,
615
+ components: @components, accessibility: @accessibility,
616
+ themes: @themes, contrast_pairs: @contrast_pairs,
617
+ forbid_literal_colors: @forbid_literal_colors
618
+ }
619
+ end
620
+ end
621
+
622
+ class DesignThemeBuilder
623
+ def initialize(name, inherits: nil)
624
+ @name = name.to_s
625
+ @inherits = inherits&.to_s
626
+ @tokens = []
627
+ end
628
+
629
+ DesignSystemBuilder::TOKEN_KINDS.each do |kind|
630
+ define_method(kind) do |name, value: nil|
631
+ @tokens << { kind:, name: name.to_s, value: value&.to_s }
632
+ end
633
+ end
634
+
635
+ def to_h = { name: @name, inherits: @inherits, tokens: @tokens }
636
+ end
637
+
638
+ class EnumerationBuilder
639
+ attr_reader :name
640
+
641
+ def initialize(name)
642
+ @name = name.to_s
643
+ @values = []
644
+ @doc = ""
645
+ end
646
+
647
+ def value(name, caption: nil)
648
+ @values << { name: name.to_s, caption: caption&.to_s }
649
+ end
650
+
651
+ def documentation(d) = (@doc = d)
652
+
653
+ def to_h
654
+ { name: @name, values: @values, documentation: @doc }
655
+ end
656
+ end
657
+
658
+ class ConstantBuilder
659
+ CONSTANT_TYPES = {
660
+ string: "DataTypes$StringType", integer: "DataTypes$IntegerType",
661
+ boolean: "DataTypes$BooleanType", decimal: "DataTypes$DecimalType",
662
+ datetime: "DataTypes$DateTimeType"
663
+ }.freeze
664
+
665
+ attr_reader :name
666
+
667
+ def initialize(name, type:, value: nil)
668
+ @name = name.to_s
669
+ @type = type.to_sym
670
+ @value = value
671
+ @doc = ""
672
+ end
673
+
674
+ def documentation(d) = (@doc = d)
675
+
676
+ def to_h
677
+ { name: @name, type: @type, value: @value, documentation: @doc }
678
+ end
679
+ end
680
+
681
+ class ScheduledEventBuilder
682
+ INTERVAL_UNITS = {
683
+ milliseconds: "Millisecond", seconds: "Second", minutes: "Minute",
684
+ hours: "Hour", days: "Day", weeks: "Week", months: "Month", years: "Year"
685
+ }.freeze
686
+
687
+ attr_reader :name
688
+
689
+ def initialize(name, microflow:, interval: 1, unit: :days, enabled: true)
690
+ @name = name.to_s
691
+ @microflow = microflow.to_s
692
+ @interval = interval.to_i
693
+ @unit = unit.to_sym
694
+ @enabled = enabled
695
+ @doc = ""
696
+ end
697
+
698
+ def documentation(d) = (@doc = d)
699
+
700
+ def to_h
701
+ {
702
+ name: @name, microflow: @microflow, interval: @interval,
703
+ unit: @unit, enabled: @enabled, documentation: @doc
704
+ }
705
+ end
706
+ end
707
+
708
+ class ModuleBuilder # rubocop:disable Metrics/ClassLength
709
+ attr_reader :name, :entities, :pages, :microflows, :nanoflows, :repositories,
710
+ :associations, :menus, :module_roles, :enumerations, :constants,
711
+ :scheduled_events, :native_documents
712
+
713
+ def initialize(name)
714
+ @name = name.to_s
715
+ @entities = []
716
+ @pages = []
717
+ @microflows = []
718
+ @nanoflows = []
719
+ @repositories = []
720
+ @associations = []
721
+ @menus = []
722
+ @module_roles = []
723
+ @enumerations = []
724
+ @constants = []
725
+ @scheduled_events = []
726
+ @native_documents = []
727
+ end
728
+
729
+ def entity(name, &block)
730
+ eb = EntityBuilder.new(name)
731
+ eb.instance_eval(&block) if block
732
+ @entities << eb.to_h
733
+ end
734
+
735
+ def page(name, &block)
736
+ pb = PageBuilder.new(name)
737
+ pb.instance_eval(&block) if block
738
+ @pages << pb.to_h
739
+ end
740
+
741
+ def menu(name, &block)
742
+ mb = MenuBuilder.new(name)
743
+ mb.instance_eval(&block) if block
744
+ @menus << mb.to_h
745
+ end
746
+
747
+ def module_role(name, description: "")
748
+ @module_roles << { name: name.to_s, description: description.to_s }
749
+ end
750
+
751
+ def microflow(name, kind: :use_case, public: false, &block)
752
+ fb = FlowBuilder.new(name, runtime: :server, kind: kind, public: public)
753
+ fb.instance_eval(&block) if block
754
+ @microflows << fb.to_h
755
+ end
756
+
757
+ def nanoflow(name, public: false, &block)
758
+ fb = FlowBuilder.new(name, runtime: :client, kind: :client_action, public: public)
759
+ fb.instance_eval(&block) if block
760
+ @nanoflows << fb.to_h
761
+ end
762
+
763
+ def query(name, public: false, &block)
764
+ microflow(name, kind: :query, public: public, &block)
765
+ end
766
+
767
+ def repository(name, implementation: nil, public: false, documentation: "")
768
+ @repositories << {
769
+ name: name.to_s, implementation: implementation&.to_s,
770
+ public: public, documentation: documentation.to_s
771
+ }
772
+ end
773
+
774
+ def enumeration(name, &block)
775
+ eb = EnumerationBuilder.new(name)
776
+ eb.instance_eval(&block) if block
777
+ @enumerations << eb.to_h
778
+ end
779
+
780
+ def constant(name, type:, value: nil, &block)
781
+ cb = ConstantBuilder.new(name, type: type, value: value)
782
+ cb.instance_eval(&block) if block
783
+ @constants << cb.to_h
784
+ end
785
+
786
+ def scheduled_event(name, microflow:, interval: 1, unit: :days, enabled: true, &block)
787
+ sb = ScheduledEventBuilder.new(
788
+ name, microflow: microflow, interval: interval, unit: unit, enabled: enabled
789
+ )
790
+ sb.instance_eval(&block) if block
791
+ @scheduled_events << sb.to_h
792
+ end
793
+
794
+ def native_document(name, type:, deep_structure:, containment: 'Documents')
795
+ raise ArgumentError, 'deep_structure requires a Hash' unless deep_structure.is_a?(Hash)
796
+
797
+ @native_documents << {
798
+ name: name.to_s, type: type.to_s, containment: containment.to_s,
799
+ doc: { '$Type' => type.to_s, 'Name' => name.to_s }.merge(deep_structure)
800
+ }
801
+ end
802
+
803
+ # Native responsive application shell. It deliberately uses only Mendix
804
+ # core widgets, so generated projects do not depend on Atlas Core merely
805
+ # to display their navigation.
806
+ def layout(name = :ApplicationLayout, title: nil, navigation: :Responsive)
807
+ appearance = lambda do |class_name = ''|
808
+ {
809
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Appearance',
810
+ 'Class' => class_name, 'DesignProperties' => IO::BsonCodec.build_array([]),
811
+ 'DynamicClasses' => '', 'Style' => ''
812
+ }
813
+ end
814
+ placeholder = {
815
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Placeholder',
816
+ 'Appearance' => appearance.call, 'Name' => 'Main', 'TabIndex' => 0
817
+ }
818
+ text = lambda do |value|
819
+ {
820
+ '$ID' => SecureRandom.uuid, '$Type' => 'Texts$Text',
821
+ 'Items' => IO::BsonCodec.build_array([
822
+ {
823
+ '$ID' => SecureRandom.uuid, '$Type' => 'Texts$Translation',
824
+ 'LanguageCode' => 'en_US', 'Text' => value.to_s
825
+ }
826
+ ])
827
+ }
828
+ end
829
+ client_template = lambda do |value|
830
+ {
831
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$ClientTemplate',
832
+ 'Fallback' => text.call(''),
833
+ 'Parameters' => IO::BsonCodec.build_array([], marker: 2),
834
+ 'Template' => text.call(value)
835
+ }
836
+ end
837
+ no_action = {
838
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$NoAction',
839
+ 'DisabledDuringExecution' => true
840
+ }
841
+ navigation_tree = {
842
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$NavigationTree',
843
+ 'Appearance' => appearance.call('mxrb-navigation'),
844
+ 'MenuSource' => {
845
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$NavigationSource',
846
+ 'NavigationProfile' => navigation.to_s
847
+ },
848
+ 'Name' => 'navigationTree1', 'TabIndex' => 0
849
+ }
850
+ left = {
851
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$ScrollContainerRegion',
852
+ 'Appearance' => appearance.call('region-sidebar'),
853
+ 'Size' => 264, 'SizeMode' => 'Pixels',
854
+ 'ToggleMode' => 'ShrinkContentInitiallyClosed',
855
+ 'Widgets' => IO::BsonCodec.build_array([navigation_tree], marker: 2)
856
+ }
857
+ sidebar_toggle = {
858
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$SidebarToggleButton',
859
+ 'Appearance' => appearance.call('mxrb-sidebar-toggle'),
860
+ 'ButtonStyle' => 'Primary', 'CaptionTemplate' => client_template.call('Menu'),
861
+ 'ConditionalVisibilitySettings' => nil, 'Icon' => nil,
862
+ 'Name' => 'sidebarToggle1', 'RenderType' => 'Button', 'TabIndex' => 0,
863
+ 'Tooltip' => text.call('Toggle navigation')
864
+ }
865
+ brand = {
866
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$DynamicText',
867
+ 'Appearance' => appearance.call('mxrb-brand'), 'Class' => '',
868
+ 'ConditionalVisibilitySettings' => nil,
869
+ 'Content' => client_template.call(title || name.to_s),
870
+ 'Name' => 'applicationTitle', 'NativeAccessibilitySettings' => nil,
871
+ 'NativeTextStyle' => 'Text', 'RenderMode' => 'Text', 'Style' => '',
872
+ 'TabIndex' => 0
873
+ }
874
+ topbar_content = {
875
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$DivContainer',
876
+ 'Appearance' => appearance.call('mxrb-topbar'),
877
+ 'ConditionalVisibilitySettings' => nil, 'Name' => 'topbarContent',
878
+ 'OnClickAction' => no_action, 'RenderMode' => 'Div',
879
+ 'ScreenReaderHidden' => false, 'TabIndex' => 0,
880
+ 'Widgets' => IO::BsonCodec.build_array([sidebar_toggle, brand], marker: 2)
881
+ }
882
+ top = {
883
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$ScrollContainerRegion',
884
+ 'Appearance' => appearance.call('region-topbar'),
885
+ 'Size' => 72, 'SizeMode' => 'Pixels', 'ToggleMode' => 'None',
886
+ 'Widgets' => IO::BsonCodec.build_array([topbar_content], marker: 2)
887
+ }
888
+ center = {
889
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$ScrollContainerRegion',
890
+ 'Appearance' => appearance.call('region-content'),
891
+ 'Size' => 200, 'SizeMode' => 'Auto',
892
+ 'ToggleMode' => 'None',
893
+ 'Widgets' => IO::BsonCodec.build_array([placeholder], marker: 2)
894
+ }
895
+ container = {
896
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$ScrollContainer',
897
+ 'Alignment' => 'Center', 'Bottom' => nil, 'CenterRegion' => center,
898
+ 'Appearance' => appearance.call, 'LayoutMode' => 'Headline', 'Left' => left,
899
+ 'Name' => 'scrollContainer1', 'Right' => nil, 'ScrollBehavior' => 'PerRegion',
900
+ 'NativeHideScrollbars' => false, 'TabIndex' => 0, 'Top' => top,
901
+ 'Width' => 960, 'WidthMode' => 'Auto'
902
+ }
903
+ content = {
904
+ '$ID' => SecureRandom.uuid, '$Type' => 'Forms$WebLayoutContent',
905
+ 'LayoutCall' => nil, 'LayoutType' => 'Responsive',
906
+ 'Widgets' => IO::BsonCodec.build_array([container], marker: 2)
907
+ }
908
+ native_document(
909
+ name, type: 'Forms$Layout', deep_structure: {
910
+ 'Appearance' => appearance.call('mxrb-application-shell'), 'CanvasHeight' => 600,
911
+ 'CanvasWidth' => 800, 'Content' => content, 'Documentation' => '',
912
+ 'Excluded' => false, 'ExportLevel' => 'Hidden'
913
+ }
914
+ )
915
+ end
916
+
917
+ def evaluate(path)
918
+ instance_eval(File.read(path), path, 1)
919
+ end
920
+
921
+ def evaluate_dir(dir)
922
+ return unless File.directory?(dir)
923
+
924
+ Dir[File.join(dir, '*.rb')].sort.each { |path| evaluate(path) }
925
+ end
926
+
927
+ def to_h
928
+ {
929
+ name: @name, entities: @entities, pages: @pages,
930
+ microflows: @microflows, nanoflows: @nanoflows,
931
+ repositories: @repositories, menus: @menus, module_roles: @module_roles,
932
+ enumerations: @enumerations, constants: @constants,
933
+ scheduled_events: @scheduled_events, native_documents: @native_documents
934
+ }
935
+ end
936
+ end
937
+
938
+ class MenuBuilder
939
+ def initialize(name)
940
+ @name = name.to_s
941
+ @items = []
942
+ @deep_structure = nil
943
+ end
944
+
945
+ def deep_structure(value)
946
+ raise ArgumentError, "deep_structure requires a Hash" unless value.is_a?(Hash)
947
+
948
+ @deep_structure = value
949
+ end
950
+
951
+ def bson_binary(base64, subtype: :generic)
952
+ BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
953
+ end
954
+
955
+ def item(caption, page: nil, &block)
956
+ child = self.class.new(caption)
957
+ child.instance_eval(&block) if block
958
+ @items << { caption: caption.to_s, page: page&.to_s, items: child.items }.compact
959
+ end
960
+
961
+ def items = @items
962
+
963
+ def to_h
964
+ { name: @name, items: @items, deep_structure: @deep_structure }
965
+ end
966
+ end
967
+
968
+ class EntityBuilder # rubocop:disable Metrics/ClassLength
969
+ ATTR_TYPES = %i[string integer long float decimal boolean datetime autonumber hashstring binary enum].freeze
970
+ ASSOCIATION_TYPES = %i[Reference ReferenceSet].freeze
971
+ ASSOCIATION_OWNERS = %i[Default Both].freeze
972
+ ASSOCIATION_STORAGE_FORMATS = %i[Column Table].freeze
973
+ CARDINALITIES = {
974
+ many_to_one: %i[Reference Default],
975
+ one_to_one: %i[Reference Both],
976
+ many_to_many: %i[ReferenceSet Default]
977
+ }.freeze
978
+
979
+ attr_reader :name
980
+
981
+ def initialize(name)
982
+ @name = name.to_s
983
+ @attributes = []
984
+ @persistable = true
985
+ @doc = ""
986
+ @associations = []
987
+ @lifecycle = nil
988
+ @access_rules = nil
989
+ @generalization = nil
990
+ @system_members = nil
991
+ @indexes = nil
992
+ end
993
+
994
+ ATTR_TYPES.each do |type|
995
+ define_method(type) do |attr_name, **opts|
996
+ @attributes << { name: attr_name.to_s, type: type, **opts }
997
+ end
998
+ end
999
+
1000
+ def non_persistent! = (@persistable = false)
1001
+ def documentation(d) = (@doc = d)
1002
+
1003
+ def generalizes(entity)
1004
+ @generalization = entity.to_s
1005
+ end
1006
+
1007
+ def system_members(owner: false, created_date: false, changed_date: false, changed_by: false)
1008
+ @system_members = {
1009
+ owner: owner, created_date: created_date,
1010
+ changed_date: changed_date, changed_by: changed_by
1011
+ }
1012
+ end
1013
+
1014
+ def index(*attributes, include_offline: false, ascending: true)
1015
+ members = attributes.flatten.map(&:to_s)
1016
+ raise ArgumentError, 'index requires at least one attribute' if members.empty?
1017
+
1018
+ @indexes ||= []
1019
+ directions = Array(ascending)
1020
+ if directions.size != 1 && directions.size != members.size
1021
+ raise ArgumentError, 'ascending must be one boolean or one value per indexed attribute'
1022
+ end
1023
+ directions *= members.size if directions.size == 1
1024
+ @indexes << {
1025
+ attributes: members, ascending: directions.map { _1 == true },
1026
+ include_offline: include_offline == true
1027
+ }
1028
+ end
1029
+
1030
+ def association(target, type: :Reference, owner: :Default, name: nil, cardinality: nil,
1031
+ documentation: '', parent_delete: :NoAction, child_delete: :NoAction,
1032
+ storage_format: nil)
1033
+ if cardinality
1034
+ type, owner = CARDINALITIES.fetch(cardinality.to_sym) do
1035
+ raise ArgumentError, 'cardinality must be :many_to_one, :one_to_one, or :many_to_many'
1036
+ end
1037
+ end
1038
+ type = type.to_sym
1039
+ owner = owner.to_sym
1040
+ unless ASSOCIATION_TYPES.include?(type)
1041
+ raise ArgumentError, "association type must be Reference or ReferenceSet"
1042
+ end
1043
+ raise ArgumentError, "association owner must be Default or Both" unless ASSOCIATION_OWNERS.include?(owner)
1044
+ storage_format = normalize_association_storage_format(storage_format)
1045
+
1046
+ @associations << {
1047
+ name: (name || "#{@name}_#{target}").to_s,
1048
+ target: target.to_s,
1049
+ type: type,
1050
+ owner: owner,
1051
+ documentation: documentation.to_s,
1052
+ parent_delete: parent_delete.to_sym,
1053
+ child_delete: child_delete.to_sym,
1054
+ storage_format: storage_format
1055
+ }
1056
+ end
1057
+
1058
+ %i[before_commit after_commit before_delete after_delete].each do |event|
1059
+ define_method(event) do |microflow:|
1060
+ @lifecycle ||= []
1061
+ @lifecycle << { event: event, handler: microflow.to_s }
1062
+ end
1063
+ end
1064
+
1065
+ # access_rule "Module.Role", create: true, delete: false, read: :all, write: [:Name]
1066
+ def access_rule(*roles, create: false, delete: false, read: :none, write: :none, xpath: "",
1067
+ default_rights: nil, members: nil)
1068
+ @access_rules ||= []
1069
+ @access_rules << {
1070
+ roles: roles.map(&:to_s),
1071
+ create: create,
1072
+ delete: delete,
1073
+ read: normalize_access(read),
1074
+ write: normalize_access(write),
1075
+ xpath: xpath.to_s,
1076
+ default_rights: default_rights&.to_s,
1077
+ members: members&.map { _1.transform_keys(&:to_sym) }
1078
+ }
1079
+ end
1080
+
1081
+ def to_h
1082
+ {
1083
+ name: @name, persistable: @persistable, documentation: @doc,
1084
+ attributes: @attributes, associations: @associations, lifecycle: @lifecycle,
1085
+ access_rules: @access_rules, generalization: @generalization,
1086
+ system_members: @system_members, indexes: @indexes
1087
+ }
1088
+ end
1089
+
1090
+ private
1091
+
1092
+ def normalize_association_storage_format(value)
1093
+ return if value.nil?
1094
+
1095
+ format = value.to_sym
1096
+ return format if ASSOCIATION_STORAGE_FORMATS.include?(format)
1097
+
1098
+ raise ArgumentError, "association storage format must be Column or Table"
1099
+ end
1100
+
1101
+ def normalize_access(value)
1102
+ case value
1103
+ when :all, :none then value
1104
+ when Array then value.map(&:to_s)
1105
+ else value.to_sym
1106
+ end
1107
+ end
1108
+ end
1109
+
1110
+ class PageBuilder
1111
+ include WidgetDsl
1112
+
1113
+ def initialize(name)
1114
+ @name = name.to_s
1115
+ @layout = "Atlas_Default"
1116
+ @title = name.to_s
1117
+ @popup = false
1118
+ @data_source = nil
1119
+ @events = []
1120
+ @widgets = []
1121
+ @allowed_roles = nil
1122
+ @deep_structure = nil
1123
+ end
1124
+
1125
+ def layout(l) = (@layout = l)
1126
+ def title(t) = (@title = t)
1127
+ def popup! = (@popup = true)
1128
+
1129
+ # Full, editable Mendix page payload for structures without a concise
1130
+ # typed DSL yet (layout grids, custom widgets, list views, etc.).
1131
+ def deep_structure(value)
1132
+ raise ArgumentError, "deep_structure requires a Hash" unless value.is_a?(Hash)
1133
+
1134
+ @deep_structure = value
1135
+ end
1136
+
1137
+ def bson_binary(base64, subtype: :generic)
1138
+ BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
1139
+ end
1140
+
1141
+ def allowed_roles(*roles)
1142
+ @allowed_roles = roles.map(&:to_s)
1143
+ end
1144
+
1145
+ def data_source(query: nil, microflow: nil, nanoflow: nil)
1146
+ choices = { microflow: microflow || query, nanoflow: nanoflow }.compact
1147
+ raise ArgumentError, "data_source requires exactly one target" unless choices.size == 1
1148
+ @data_source = { kind: choices.keys.first, name: choices.values.first.to_s }
1149
+ end
1150
+
1151
+ %i[on_change on_click on_submit on_load].each do |event|
1152
+ define_method(event) do |target: nil, microflow: nil, nanoflow: nil, action: nil|
1153
+ choices = { microflow: microflow, nanoflow: nanoflow, action: action }.compact
1154
+ raise ArgumentError, "#{event} requires exactly one handler" unless choices.size == 1
1155
+ @events << {
1156
+ event: event, target: target&.to_s,
1157
+ kind: choices.keys.first, handler: choices.values.first.to_s
1158
+ }
1159
+ end
1160
+ end
1161
+
1162
+ def to_h
1163
+ {
1164
+ name: @name, layout: @layout, title: @title, popup: @popup,
1165
+ data_source: @data_source, events: @events, widgets: @widgets,
1166
+ allowed_roles: @allowed_roles, deep_structure: @deep_structure
1167
+ }
1168
+ end
1169
+
1170
+ private
1171
+
1172
+ def _widget_list = @widgets
1173
+ end
1174
+
1175
+ # Shared activity DSL mixed into FlowBuilder, BranchBuilder, LoopBuilder, RescueBuilder
1176
+ module FlowBodyDsl
1177
+ def create_object(entity, as:, set: {}, commit: false, with_events: true,
1178
+ refresh: false, &block)
1179
+ _acts << { type: :create_object, entity: entity.to_s, variable: as.to_s,
1180
+ members: _build_members(set, &block), commit: commit,
1181
+ with_events: with_events, refresh: refresh }
1182
+ end
1183
+
1184
+ def change_object(variable, set: {}, commit: false, with_events: true,
1185
+ refresh: false, &block)
1186
+ _acts << { type: :change_object, variable: variable.to_s,
1187
+ members: _build_members(set, &block), commit: commit,
1188
+ with_events: with_events, refresh: refresh }
1189
+ end
1190
+
1191
+ def retrieve_objects(entity, as:, xpath: nil, limit: nil, single: false, sort: [])
1192
+ _acts << { type: :retrieve_objects, entity: entity.to_s, variable: as.to_s,
1193
+ xpath: xpath&.to_s, limit: limit, single: single,
1194
+ sortings: Array(sort) }
1195
+ end
1196
+
1197
+ def retrieve_association(from, association:, as:)
1198
+ _acts << {
1199
+ type: :retrieve_association, variable: as.to_s,
1200
+ start_variable: from.to_s, association: association.to_s
1201
+ }
1202
+ end
1203
+
1204
+ def commit(variable, with_events: true, refresh: false)
1205
+ _acts << {
1206
+ type: :commit, variable: variable.to_s,
1207
+ with_events: with_events, refresh: refresh
1208
+ }
1209
+ end
1210
+
1211
+ def delete(variable, refresh: false)
1212
+ _acts << { type: :delete_object, variable: variable.to_s, refresh: refresh }
1213
+ end
1214
+
1215
+ def call_microflow(name, as: nil, pass: {}, result_name: nil, use_return: nil)
1216
+ mappings = pass.map { |param, var| { param: param.to_s, value: var } }
1217
+ _acts << {
1218
+ type: :call_microflow, name: name.to_s, variable: as&.to_s,
1219
+ result_name: result_name&.to_s,
1220
+ use_return: use_return.nil? ? !as.nil? : use_return == true,
1221
+ mappings: mappings
1222
+ }
1223
+ end
1224
+
1225
+ %i[java javascript nanoflow app_service].each do |runtime|
1226
+ define_method(:"call_#{runtime}") do |name, as: nil, pass: {},
1227
+ result_name: nil, use_return: nil|
1228
+ mappings = pass.map { |param, value| { param: param.to_s, value: value } }
1229
+ _acts << {
1230
+ type: :"call_#{runtime}", name: name.to_s,
1231
+ variable: as&.to_s, result_name: result_name&.to_s,
1232
+ use_return: use_return.nil? ? !as.nil? : use_return == true,
1233
+ mappings: mappings
1234
+ }
1235
+ end
1236
+ end
1237
+
1238
+ def create_variable(name, type: nil, value: nil)
1239
+ _acts << {
1240
+ type: :create_variable, variable: name.to_s,
1241
+ variable_type: type&.to_s, value: value
1242
+ }
1243
+ end
1244
+
1245
+ def change_variable(name, to:)
1246
+ _acts << { type: :change_variable, variable: name.to_s, value: to }
1247
+ end
1248
+
1249
+ def show_message(text = nil, type: :information, blocking: false,
1250
+ translations: nil, parameters: [])
1251
+ _acts << {
1252
+ type: :show_message, text: text.to_s, message_type: type.to_s,
1253
+ blocking: blocking,
1254
+ translations: translations&.transform_keys(&:to_s),
1255
+ parameters: Array(parameters)
1256
+ }
1257
+ end
1258
+
1259
+ def log_message(message, level: :info, node: nil, include_stack: false, parameters: [])
1260
+ _acts << {
1261
+ type: :log_message, message: message.to_s, level: level.to_s,
1262
+ node: node&.to_s, include_stack: include_stack,
1263
+ parameters: Array(parameters)
1264
+ }
1265
+ end
1266
+
1267
+ def show_page(page, object: nil, location: nil, pass: {}, close_pages: nil,
1268
+ title: nil)
1269
+ _acts << {
1270
+ type: :show_page, page: page.to_s, variable: object&.to_s,
1271
+ location: location&.to_s,
1272
+ mappings: pass.map { |parameter, value| { parameter: parameter.to_s, value: value } },
1273
+ close_pages: close_pages, title: title
1274
+ }
1275
+ end
1276
+
1277
+ def close_page(count: nil)
1278
+ _acts << { type: :close_page, count: count }
1279
+ end
1280
+
1281
+ def aggregate(list, function:, as:, attribute: nil)
1282
+ _acts << {
1283
+ type: :aggregate, variable: list.to_s, function: function.to_s,
1284
+ output: as.to_s, attribute: attribute.to_s
1285
+ }
1286
+ end
1287
+
1288
+ def rollback(variable, refresh: false)
1289
+ _acts << { type: :rollback, variable: variable.to_s, refresh: refresh }
1290
+ end
1291
+
1292
+ def cast(variable)
1293
+ _acts << { type: :cast, variable: variable.to_s }
1294
+ end
1295
+
1296
+ def create_list(entity, as:)
1297
+ _acts << { type: :create_list, entity: entity.to_s, variable: as.to_s }
1298
+ end
1299
+
1300
+ def list_operation(operation, list, as:, with: nil, expression: nil)
1301
+ _acts << {
1302
+ type: :list_operation, operation: operation.to_s,
1303
+ variable: list.to_s, second: with&.to_s,
1304
+ expression: expression&.to_s, output: as.to_s
1305
+ }
1306
+ end
1307
+
1308
+ def change_list(list, action:, value:)
1309
+ _acts << {
1310
+ type: :change_list, variable: list.to_s,
1311
+ action: action.to_s, value: value
1312
+ }
1313
+ end
1314
+
1315
+ def validation_feedback(variable, attribute: nil, association: nil,
1316
+ translations: {}, parameters: [], error: :rollback)
1317
+ _acts << {
1318
+ type: :validation_feedback, variable: variable.to_s,
1319
+ attribute: attribute.to_s, association: association.to_s,
1320
+ translations: translations.transform_keys(&:to_s),
1321
+ parameters: Array(parameters), error: error.to_s
1322
+ }
1323
+ end
1324
+
1325
+ def call_rest(method:, location:, location_parameters: [], headers: {},
1326
+ request_mapping: nil, request_variable: nil,
1327
+ result_mapping: nil, as: nil, result_entity: nil,
1328
+ timeout: nil, commit: :yes_without_events,
1329
+ error_result: :http_response, error: :rollback)
1330
+ _acts << {
1331
+ type: :call_rest, method: method.to_s, location: location.to_s,
1332
+ location_parameters: Array(location_parameters),
1333
+ headers: headers.transform_keys(&:to_s),
1334
+ request_mapping: request_mapping&.to_s,
1335
+ request_variable: request_variable&.to_s,
1336
+ result_mapping: result_mapping&.to_s, variable: as&.to_s,
1337
+ result_entity: result_entity&.to_s, timeout: timeout&.to_s,
1338
+ commit: commit.to_s, error_result: error_result.to_s,
1339
+ error: error.to_s
1340
+ }
1341
+ end
1342
+
1343
+ def return_value(value)
1344
+ expression = value.is_a?(Symbol) ? "$#{value}" : value.to_s
1345
+ _acts << { type: :return_event, expression: expression }
1346
+ end
1347
+
1348
+ def end_flow
1349
+ _acts << { type: :return_event, expression: "" }
1350
+ end
1351
+
1352
+ def error_event
1353
+ _acts << { type: :error_event }
1354
+ end
1355
+
1356
+ def continue_loop
1357
+ _acts << { type: :continue_event }
1358
+ end
1359
+
1360
+ def rescue_all(&block)
1361
+ rescue_builder = RescueBuilder.new
1362
+ rescue_builder.instance_eval(&block) if block
1363
+ _acts << {
1364
+ type: :rescue_all,
1365
+ activities: rescue_builder.activities
1366
+ }
1367
+ end
1368
+
1369
+ def decision(condition, &block)
1370
+ db = DecisionBuilder.new(condition.is_a?(Hash) ? condition : condition.to_s)
1371
+ db.instance_eval(&block) if block
1372
+ _acts << db.to_h
1373
+ end
1374
+
1375
+ def type_decision(variable, &block)
1376
+ builder = TypeDecisionBuilder.new(variable.to_s)
1377
+ builder.instance_eval(&block) if block
1378
+ _acts << builder.to_h
1379
+ end
1380
+
1381
+ def loop_over(variable, as: nil, &block)
1382
+ lb = LoopBuilder.new(variable.to_s, as: (as || variable).to_s)
1383
+ lb.instance_eval(&block) if block
1384
+ _acts << lb.to_h
1385
+ end
1386
+
1387
+ def while_loop(condition, &block)
1388
+ lb = LoopBuilder.new(nil, as: nil)
1389
+ lb.instance_eval(&block) if block
1390
+ item = lb.to_h
1391
+ item[:type] = :while_loop
1392
+ item[:condition] = condition.to_s
1393
+ _acts << item
1394
+ end
1395
+
1396
+ private
1397
+
1398
+ def _build_members(set_hash, &block)
1399
+ if block
1400
+ sb = SetBuilder.new
1401
+ sb.instance_eval(&block)
1402
+ sb.members
1403
+ else
1404
+ set_hash.map { |k, v| { attribute: k.to_s, value: v } }
1405
+ end
1406
+ end
1407
+ end
1408
+
1409
+ class FlowBuilder
1410
+ include FlowBodyDsl
1411
+
1412
+ def initialize(name, runtime:, kind:, public:)
1413
+ @name = name.to_s
1414
+ @runtime = runtime
1415
+ @kind = kind
1416
+ @public = public
1417
+ @parameters = []
1418
+ @return_type = nil
1419
+ @doc = ""
1420
+ @calls = []
1421
+ @repositories = []
1422
+ @allowed_roles = nil
1423
+ @body = nil
1424
+ @return_variable_name = nil
1425
+ @return_expression = nil
1426
+ @expected_body_fingerprint = nil
1427
+ @allow_concurrent_execution = nil
1428
+ @mark_as_used = nil
1429
+ @excluded = nil
1430
+ end
1431
+
1432
+ def parameter(name, type:)
1433
+ @parameters << { name: name.to_s, type: type.is_a?(Hash) ? type : type.to_s }
1434
+ end
1435
+
1436
+ def bson_binary(base64, subtype: :generic)
1437
+ BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
1438
+ end
1439
+
1440
+ def return_type(t) = (@return_type = t.to_s)
1441
+ def documentation(d) = (@doc = d)
1442
+ def allow_concurrent_execution(value = true) = (@allow_concurrent_execution = !!value)
1443
+ def mark_as_used(value = true) = (@mark_as_used = !!value)
1444
+ def excluded(value = true) = (@excluded = !!value)
1445
+
1446
+ def allowed_roles(*roles)
1447
+ @allowed_roles = roles.map(&:to_s)
1448
+ end
1449
+
1450
+ def call(microflow: nil, nanoflow: nil)
1451
+ choices = { microflow: microflow, nanoflow: nanoflow }.compact
1452
+ raise ArgumentError, "call requires exactly one target" unless choices.size == 1
1453
+ @calls << { kind: choices.keys.first, name: choices.values.first.to_s }
1454
+ end
1455
+
1456
+ def uses_repository(name)
1457
+ @repositories << name.to_s
1458
+ end
1459
+
1460
+ def rescue_all(&block)
1461
+ rb = RescueBuilder.new
1462
+ rb.instance_eval(&block) if block
1463
+ _acts << { type: :rescue_all, activities: rb.activities }
1464
+ end
1465
+
1466
+ def return_value(var)
1467
+ if var.is_a?(Symbol)
1468
+ @return_variable_name = var.to_s
1469
+ @return_expression = "$#{var}"
1470
+ else
1471
+ @return_expression = var.to_s
1472
+ end
1473
+ end
1474
+
1475
+ def body_fingerprint(value)
1476
+ @expected_body_fingerprint = value.to_s
1477
+ end
1478
+
1479
+ def self.body_digest(body, return_expression)
1480
+ payload = canonical_body_value([body, return_expression])
1481
+ Digest::SHA256.hexdigest(JSON.generate(payload))
1482
+ end
1483
+
1484
+ def self.canonical_body_value(value)
1485
+ case value
1486
+ when Hash
1487
+ value.keys.sort_by(&:to_s).to_h do |key|
1488
+ [key.to_s, canonical_body_value(value[key])]
1489
+ end
1490
+ when Array
1491
+ value.map { canonical_body_value(_1) }
1492
+ when Symbol
1493
+ { "__symbol__" => value.to_s }
1494
+ else
1495
+ value
1496
+ end
1497
+ end
1498
+
1499
+ def to_h
1500
+ current_fingerprint = self.class.body_digest(@body, @return_expression)
1501
+ {
1502
+ name: @name, runtime: @runtime, kind: @kind, public: @public,
1503
+ parameters: @parameters, return_type: @return_type, documentation: @doc,
1504
+ calls: @calls, repositories: @repositories, allowed_roles: @allowed_roles,
1505
+ body: @body, return_variable_name: @return_variable_name,
1506
+ return_expression: @return_expression,
1507
+ allow_concurrent_execution: @allow_concurrent_execution,
1508
+ mark_as_used: @mark_as_used, excluded: @excluded,
1509
+ preserve_native_body: !@expected_body_fingerprint.nil? &&
1510
+ @expected_body_fingerprint == current_fingerprint
1511
+ }
1512
+ end
1513
+
1514
+ private
1515
+
1516
+ def _acts = (@body ||= [])
1517
+ end
1518
+
1519
+ class DecisionBuilder
1520
+ def initialize(condition)
1521
+ @condition = condition
1522
+ @true_branch = []
1523
+ @false_branch = []
1524
+ @branches = {}
1525
+ end
1526
+
1527
+ def on(value, &block)
1528
+ bb = BranchBuilder.new
1529
+ bb.instance_eval(&block) if block
1530
+ case value
1531
+ when true then @true_branch = bb.activities
1532
+ when false then @false_branch = bb.activities
1533
+ end
1534
+ @branches[value] = bb.activities
1535
+ end
1536
+
1537
+ def to_h
1538
+ { type: :decision, condition: @condition,
1539
+ true_branch: @true_branch, false_branch: @false_branch,
1540
+ branches: @branches }
1541
+ end
1542
+ end
1543
+
1544
+ class BranchBuilder
1545
+ include FlowBodyDsl
1546
+ attr_reader :activities
1547
+
1548
+ def initialize
1549
+ @activities = []
1550
+ end
1551
+
1552
+ private
1553
+
1554
+ def _acts = @activities
1555
+ end
1556
+
1557
+ class TypeDecisionBuilder
1558
+ def initialize(variable)
1559
+ @variable = variable
1560
+ @branches = {}
1561
+ end
1562
+
1563
+ def on_type(type, &block)
1564
+ add_branch(type.to_s, &block)
1565
+ end
1566
+
1567
+ def otherwise(&block)
1568
+ add_branch("", &block)
1569
+ end
1570
+
1571
+ def to_h
1572
+ { type: :inheritance_decision, variable: @variable, branches: @branches }
1573
+ end
1574
+
1575
+ private
1576
+
1577
+ def add_branch(value, &block)
1578
+ branch = BranchBuilder.new
1579
+ branch.instance_eval(&block) if block
1580
+ @branches[value] = branch.activities
1581
+ end
1582
+ end
1583
+
1584
+ class LoopBuilder
1585
+ include FlowBodyDsl
1586
+
1587
+ def initialize(variable, as:)
1588
+ @variable = variable
1589
+ @iterator = as
1590
+ @activities = []
1591
+ end
1592
+
1593
+ def to_h
1594
+ { type: :loop_over, variable: @variable, iterator: @iterator, activities: @activities }
1595
+ end
1596
+
1597
+ private
1598
+
1599
+ def _acts = @activities
1600
+ end
1601
+
1602
+ class RescueBuilder
1603
+ include FlowBodyDsl
1604
+ attr_reader :activities
1605
+
1606
+ def initialize
1607
+ @activities = []
1608
+ end
1609
+
1610
+ private
1611
+
1612
+ def _acts = @activities
1613
+ end
1614
+
1615
+ class SetBuilder
1616
+ attr_reader :members
1617
+
1618
+ def initialize
1619
+ @members = []
1620
+ end
1621
+
1622
+ def set(attribute, to:)
1623
+ @members << { attribute: attribute.to_s, value: to }
1624
+ end
1625
+
1626
+ def set_association(association, to:, operation: :set)
1627
+ @members << {
1628
+ association: association.to_s, value: to,
1629
+ operation: operation.to_s
1630
+ }
1631
+ end
1632
+ end
1633
+
1634
+ MicroflowBuilder = FlowBuilder
1635
+ end
1636
+ end