mxrb 0.1.2 → 0.1.4

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mxrb +458 -10
  3. data/docs/de-DE/README.md +1 -0
  4. data/docs/de-DE/oql-sql.md +42 -0
  5. data/docs/de-DE/project-structure.md +201 -2
  6. data/docs/de-DE/runtime-ruby.md +104 -0
  7. data/docs/de-DE/runtime-schema-migrations.md +44 -0
  8. data/docs/de-DE/validation-matrix.md +42 -4
  9. data/docs/en-US/README.md +1 -0
  10. data/docs/en-US/oql-sql.md +41 -0
  11. data/docs/en-US/project-structure.md +192 -2
  12. data/docs/en-US/runtime-ruby.md +100 -0
  13. data/docs/en-US/runtime-schema-migrations.md +41 -0
  14. data/docs/en-US/validation-matrix.md +39 -4
  15. data/docs/pt-BR/README.md +1 -0
  16. data/docs/pt-BR/entity-dsl.md +8 -0
  17. data/docs/pt-BR/oql-sql.md +41 -0
  18. data/docs/pt-BR/project-structure.md +197 -2
  19. data/docs/pt-BR/runtime-ruby.md +108 -0
  20. data/docs/pt-BR/runtime-schema-migrations.md +40 -0
  21. data/docs/pt-BR/validation-matrix.md +42 -5
  22. data/docs/pt-BR/writing.md +28 -0
  23. data/lib/mxrb/cli/help.rb +426 -0
  24. data/lib/mxrb/cli/release.rb +137 -0
  25. data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
  26. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
  27. data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
  28. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
  29. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
  30. data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
  31. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
  32. data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
  33. data/lib/mxrb/compiler/packager.rb +27 -13
  34. data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
  35. data/lib/mxrb/compiler/portable_packager.rb +16 -11
  36. data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
  37. data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
  38. data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
  39. data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
  40. data/lib/mxrb/domain_diagram.rb +409 -0
  41. data/lib/mxrb/dsl/builder.rb +62 -11
  42. data/lib/mxrb/environment.rb +226 -0
  43. data/lib/mxrb/exporter.rb +265 -45
  44. data/lib/mxrb/http/server.rb +107 -0
  45. data/lib/mxrb/initializer.rb +80 -9
  46. data/lib/mxrb/integrity/validator.rb +32 -7
  47. data/lib/mxrb/io/bson_codec.rb +42 -1
  48. data/lib/mxrb/io/mpr_file.rb +146 -3
  49. data/lib/mxrb/model/attribute.rb +3 -0
  50. data/lib/mxrb/model/entity.rb +44 -2
  51. data/lib/mxrb/model/microflow.rb +3 -1
  52. data/lib/mxrb/model/module.rb +71 -0
  53. data/lib/mxrb/model/page.rb +126 -15
  54. data/lib/mxrb/model/project.rb +3 -0
  55. data/lib/mxrb/modeler/catalog.rb +178 -0
  56. data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
  57. data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
  58. data/lib/mxrb/official_marketplace.rb +9 -3
  59. data/lib/mxrb/oql/reverse_translator.rb +394 -0
  60. data/lib/mxrb/oql/server.rb +4 -8
  61. data/lib/mxrb/oql.rb +32 -4
  62. data/lib/mxrb/progress.rb +242 -0
  63. data/lib/mxrb/project_lifecycle.rb +5 -2
  64. data/lib/mxrb/ruby_app/exporter.rb +1749 -0
  65. data/lib/mxrb/ruby_app/preset.rb +506 -0
  66. data/lib/mxrb/ruby_app/session_manager.rb +122 -0
  67. data/lib/mxrb/ruby_app.rb +1361 -0
  68. data/lib/mxrb/runtime/access_control.rb +408 -0
  69. data/lib/mxrb/runtime/native.rb +1015 -93
  70. data/lib/mxrb/runtime/scheduler.rb +396 -0
  71. data/lib/mxrb/runtime/schema_migrator.rb +598 -0
  72. data/lib/mxrb/runtime/shared_store.rb +335 -0
  73. data/lib/mxrb/runtime/sqlite_store.rb +729 -0
  74. data/lib/mxrb/scaffold/generator.rb +11 -3
  75. data/lib/mxrb/team_server.rb +10 -7
  76. data/lib/mxrb/uml/activity_diagram.rb +118 -0
  77. data/lib/mxrb/uml/class_diagram.rb +118 -0
  78. data/lib/mxrb/uml/sequence_diagram.rb +112 -0
  79. data/lib/mxrb/uml/server.rb +146 -0
  80. data/lib/mxrb/uml/support.rb +35 -0
  81. data/lib/mxrb/version.rb +1 -1
  82. data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
  83. data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
  84. data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
  85. data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
  86. data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
  87. data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
  88. data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
  89. data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
  90. data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
  91. data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  92. data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
  93. data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
  94. data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  95. data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
  96. data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  97. data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
  98. data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
  99. data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
  100. data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  101. data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
  102. data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  103. data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
  104. data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
  105. data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
  106. data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
  107. data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
  108. data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
  109. data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
  110. data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  111. data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
  112. data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
  113. data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
  114. data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
  115. data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
  116. data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
  117. data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
  118. data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
  119. data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  120. data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
  121. data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
  122. data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
  123. data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
  124. data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
  125. data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
  126. data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
  127. data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
  128. data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
  129. data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
  130. data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
  131. data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
  132. data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
  133. data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
  134. data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
  135. data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
  136. data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
  137. data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
  138. data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
  139. data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
  140. data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
  141. data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
  142. data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
  143. data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
  144. data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
  145. data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
  146. data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
  147. data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
  148. data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
  149. data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
  150. data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
  151. data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
  152. data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
  153. data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
  154. data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
  155. data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
  156. data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
  157. data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
  158. data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
  159. data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
  160. data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
  161. data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
  162. data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
  163. data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
  164. data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
  165. data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
  166. data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
  167. data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
  168. data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  169. data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
  170. data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
  171. data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
  172. data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
  173. data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
  174. data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
  175. data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
  176. data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
  177. data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
  178. data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
  179. data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
  180. data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
  181. data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
  182. data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
  183. data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
  184. data/lib/mxrb/web_ui/domain.html +15 -0
  185. data/lib/mxrb/web_ui/modeler.html +16 -0
  186. data/lib/mxrb/web_ui/uml.html +31 -0
  187. data/lib/mxrb/web_ui.rb +89 -0
  188. data/lib/mxrb/widget_certification.rb +208 -0
  189. data/lib/mxrb/writer.rb +278 -41
  190. data/lib/mxrb.rb +38 -8
  191. metadata +157 -5
data/lib/mxrb/exporter.rb CHANGED
@@ -8,7 +8,9 @@ require "digest"
8
8
  module Mxrb
9
9
  # Exports an MPR into an editable, layered Ruby source tree.
10
10
  class Exporter
11
+ MODES = %i[mendix ruby].freeze
11
12
  LAYERS = %w[domain application presentation infrastructure].freeze
13
+ MODULE_PROGRESS_WEIGHT = 10
12
14
  MARKETPLACE_PROVENANCE = [
13
15
  ".mxrb/marketplace.lock.json",
14
16
  ".mxrb/marketplace",
@@ -57,40 +59,64 @@ module Mxrb
57
59
  Microflows$RestCallAction
58
60
  ].freeze
59
61
 
60
- def initialize(mpr_path, output_dir)
62
+ attr_reader :mode
63
+
64
+ def initialize(mpr_path, output_dir, mode: :mendix)
61
65
  @mpr_path = File.expand_path(mpr_path)
62
66
  @output_dir = File.expand_path(output_dir)
67
+ @mode = mode.to_sym
68
+ raise ArgumentError, "export mode must be mendix or ruby" unless MODES.include?(@mode)
63
69
  end
64
70
 
65
71
  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) }
72
+ return export_ruby!(parallel:) if mode == :ruby
73
+
74
+ Progress.with("Exporting #{File.basename(@mpr_path)}") do |progress|
75
+ FileUtils.mkdir_p(@output_dir)
76
+ Mxrb.open(@mpr_path) do |project|
77
+ @architecture = project.architecture_definition
78
+ units = project.all_units
79
+ modules = project.modules
80
+ @inferred_public_artifacts = infer_public_artifacts(modules)
81
+ assets = project_asset_files
82
+ progress.update(
83
+ current: 0,
84
+ total: 4 + assets.size + (units.size * 2) + (modules.size * MODULE_PROGRESS_WEIGHT),
85
+ detail: "preparing Ruby project"
86
+ )
87
+ export_app_structure
88
+ progress.advance(detail: "project structure")
89
+ export_project_assets(assets, progress)
90
+ export_native_units(project, units, progress)
91
+ ruby_sources = export_ruby_app_sources(project)
92
+ export_security(project)
93
+ progress.advance(detail: "project security")
94
+ export_architecture_contracts(project)
95
+ progress.advance(detail: "navigation and design system")
96
+ export_modules(modules, progress, parallel:)
97
+ write_project(project, ruby_sources:)
98
+ progress.advance(detail: "project.rb")
80
99
  end
81
- write_project(project)
82
100
  end
83
101
  @output_dir
84
102
  end
85
103
 
86
104
  private
87
105
 
106
+ def export_ruby!(parallel:)
107
+ sidecar = File.join(@output_dir, ".mxrb", "mendix")
108
+ self.class.new(@mpr_path, sidecar, mode: :mendix).export!(parallel:)
109
+ RubyApp::Exporter.new(@mpr_path, @output_dir, mendix_sidecar: sidecar).export!
110
+ end
111
+
88
112
  def export_module(mod)
89
113
  root = File.join(@output_dir, "modules", mod.name)
90
114
  LAYERS.each { FileUtils.mkdir_p(File.join(root, _1)) }
91
115
 
92
116
  export_domain(root, mod)
93
117
  export_microflows(root, mod)
118
+ export_application_documents(root, mod)
119
+ export_infrastructure_documents(root, mod)
94
120
  export_pages(root, mod)
95
121
  export_menus(root, mod)
96
122
  export_nanoflows(root, mod)
@@ -106,25 +132,31 @@ module Mxrb
106
132
  ].each { write(File.join(@output_dir, _1, ".keep"), "") }
107
133
  end
108
134
 
109
- def export_project_assets
135
+ def project_asset_files
110
136
  source_root = File.dirname(@mpr_path)
111
- files = (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory|
137
+ (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory|
112
138
  root = File.join(source_root, directory)
113
139
  next [root] if File.file?(root) && !File.symlink?(root)
114
140
  next [] unless File.directory?(root) && !File.symlink?(root)
115
141
 
116
142
  Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH)
117
143
  end.select { File.file?(_1) && !File.symlink?(_1) }.sort
144
+ end
145
+
146
+ def export_project_assets(files, progress)
147
+ source_root = File.dirname(@mpr_path)
118
148
  entries = files.map do |source|
119
149
  relative = Pathname.new(source).relative_path_from(Pathname.new(source_root)).to_s
120
150
  target = File.join(@output_dir, relative)
121
151
  FileUtils.mkdir_p(File.dirname(target))
122
152
  FileUtils.cp(source, target)
123
- {
153
+ result = {
124
154
  "path" => relative,
125
155
  "size" => File.size(source),
126
156
  "sha256" => Digest::SHA256.file(source).hexdigest
127
157
  }
158
+ progress.advance(detail: "asset #{relative}")
159
+ result
128
160
  end
129
161
  write(
130
162
  File.join(@output_dir, ".mxrb", "assets.json"),
@@ -132,8 +164,8 @@ module Mxrb
132
164
  )
133
165
  end
134
166
 
135
- def export_native_units(project)
136
- units = project.all_units.filter_map do |unit|
167
+ def export_native_units(project, project_units, progress)
168
+ units = project_units.filter_map do |unit|
137
169
  doc = project.parse_bson(unit)
138
170
  type = doc["$Type"]
139
171
  next if type.to_s.empty? || type == "Projects$Project"
@@ -147,16 +179,21 @@ module Mxrb
147
179
  "type" => type,
148
180
  "contents" => Base64.strict_encode64(IO::BsonCodec.serialize(doc))
149
181
  }
182
+ ensure
183
+ progress.advance(detail: "native baseline")
150
184
  end
151
185
  write(
152
186
  File.join(@output_dir, ".mxrb", "native_units.json"),
153
187
  JSON.pretty_generate("format_version" => project.format_version.to_s, "units" => units)
154
188
  )
155
- source = project.all_units.filter_map do |unit|
189
+ source = project_units.filter_map do |unit|
156
190
  doc = project.parse_bson(unit)
157
191
  next if doc["$Type"].to_s.empty? || doc["$Type"] == "Projects$Project"
192
+ next if Model::Module::EDITABLE_DOCUMENT_TYPES.include?(doc["$Type"])
158
193
 
159
194
  native_unit_source(project, unit, doc)
195
+ ensure
196
+ progress.advance(detail: "editable native units")
160
197
  end
161
198
  write(
162
199
  File.join(@output_dir, ".mxrb", "native_units.rb"),
@@ -164,6 +201,36 @@ module Mxrb
164
201
  )
165
202
  end
166
203
 
204
+ def export_ruby_app_sources(project)
205
+ files = project.mpr.ruby_app_sources
206
+ return false if files.empty?
207
+
208
+ payload = files.map do |file|
209
+ contents = file.fetch(:contents)
210
+ {
211
+ "path" => file.fetch(:path), "sha256" => file.fetch(:sha256),
212
+ "contents" => Base64.strict_encode64(contents), "mode" => file.fetch(:mode, 0o644)
213
+ }
214
+ end
215
+ write(
216
+ File.join(@output_dir, ".mxrb", "ruby_sources.json"),
217
+ JSON.pretty_generate("version" => 1, "files" => payload)
218
+ )
219
+ true
220
+ end
221
+
222
+ def export_modules(modules, progress, parallel:)
223
+ operation = lambda do |mod|
224
+ export_module(mod)
225
+ progress.advance(MODULE_PROGRESS_WEIGHT, detail: "module #{mod.name}")
226
+ end
227
+ if parallel && modules.size > 1
228
+ modules.map { |mod| Thread.new { operation.call(mod) } }.each(&:join)
229
+ else
230
+ modules.each { operation.call(_1) }
231
+ end
232
+ end
233
+
167
234
  def native_unit_source(project, unit, doc)
168
235
  <<~RUBY
169
236
  native_unit #{ruby(unit.fetch("UnitID"))},
@@ -211,8 +278,10 @@ module Mxrb
211
278
 
212
279
  def export_module_scaffolding(root)
213
280
  %w[
214
- domain/enumerations domain/rules domain/policies
215
- application/ports/repositories application/queries application/validations application/jobs
281
+ domain/entities domain/dtos domain/oql_views domain/enumerations domain/constants
282
+ domain/rules domain/policies
283
+ application/ports/repositories application/queries application/queries/datasets
284
+ application/validations application/jobs application/jobs/scheduled_events
216
285
  presentation/features presentation/client_actions presentation/snippets presentation/view_models
217
286
  presentation/menus
218
287
  infrastructure/persistence/mendix infrastructure/persistence/external
@@ -238,26 +307,62 @@ module Mxrb
238
307
 
239
308
  def export_domain(root, mod)
240
309
  domain = File.join(root, "domain")
241
- entities_dir = File.join(domain, "entities")
242
- FileUtils.mkdir_p(entities_dir)
243
-
244
310
  associations = mod.associations.group_by(&:from_entity_id)
245
311
  architecture = architecture_module(mod.name)
246
- entity_files = unique_entity_filenames(mod.entities)
312
+ paths = {}
313
+ grouped = mod.entities.group_by { entity_domain_route(_1) }
314
+ grouped.each do |route, entities|
315
+ unique_entity_filenames(entities, route:).each do |id, filename|
316
+ paths[id] = File.join(route, filename)
317
+ end
318
+ end
319
+ domain_documents = mod.domain_documents
320
+ oql_documents = mod.oql_view_documents
321
+ attached_oql_documents = {}
247
322
  mod.entities.each do |entity|
248
323
  entity_metadata = architecture&.fetch(:entities, [])&.find { _1[:name] == entity.name }
324
+ source = entity_source(entity, mod, associations.fetch(entity.id, []), entity_metadata)
325
+ if oql_view_entity?(entity) && (document = oql_document_for(entity, oql_documents, mod.name))
326
+ source = "#{source.rstrip}\n\n#{native_document_declaration(document)}"
327
+ attached_oql_documents[document.fetch(:id)] = true
328
+ end
249
329
  write(
250
- File.join(entities_dir, entity_files.fetch(entity.id)),
251
- entity_source(entity, mod, associations.fetch(entity.id, []), entity_metadata)
330
+ File.join(domain, paths.fetch(entity.id)), source
252
331
  )
253
332
  end
254
-
255
- loads = entity_files.values.sort.map do |filename|
256
- %(evaluate File.join(__dir__, "entities", #{ruby(filename)}))
333
+ used = paths.values.to_h { [_1, true] }
334
+ domain_documents.reject { attached_oql_documents[_1.fetch(:id)] }.each do |document|
335
+ relative = unique_relative_path(document.fetch(:route), underscore(document.fetch(:name)), used)
336
+ write(File.join(domain, relative), mapping_document_source(document))
337
+ paths[document.fetch(:id)] = relative
338
+ end
339
+ loads = paths.values.sort.map do |relative|
340
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
341
+ %(evaluate File.join(__dir__, #{segments}))
257
342
  end
258
343
  write(File.join(domain, "model.rb"), "#{loads.join("\n")}\n")
259
344
  end
260
345
 
346
+ def entity_domain_route(entity)
347
+ return 'oql_views' if oql_view_entity?(entity)
348
+ return 'dtos' unless entity.persistable
349
+
350
+ 'entities'
351
+ end
352
+
353
+ def oql_view_entity?(entity)
354
+ entity.respond_to?(:oql_view?) && entity.oql_view?
355
+ end
356
+
357
+ def oql_document_for(entity, documents, module_name)
358
+ reference = entity.oql_source_document.to_s
359
+ return nil if reference.empty?
360
+
361
+ documents.find do |document|
362
+ [document.fetch(:name), "#{module_name}.#{document.fetch(:name)}"].include?(reference)
363
+ end
364
+ end
365
+
261
366
  def export_microflows(root, mod)
262
367
  files = unique_filenames(mod.microflows)
263
368
  by_layer = { "application" => [], "infrastructure" => [] }
@@ -265,7 +370,10 @@ module Mxrb
265
370
  mod.microflows.each do |flow|
266
371
  layer, category = flow_location(flow.name)
267
372
  relative = File.join(category, files.fetch(flow.id))
268
- metadata = architecture&.fetch(:microflows, [])&.find { _1[:name] == flow.name }
373
+ metadata = flow_metadata(
374
+ mod.name, :microflow, flow.name,
375
+ architecture&.fetch(:microflows, [])&.find { _1[:name] == flow.name }
376
+ )
269
377
  write(File.join(root, layer, relative), microflow_source(flow, metadata))
270
378
  by_layer.fetch(layer) << relative
271
379
  end
@@ -296,6 +404,74 @@ module Mxrb
296
404
  write_path_aggregator(File.join(presentation, "presentation.rb"), paths)
297
405
  end
298
406
 
407
+ def export_infrastructure_documents(root, mod)
408
+ documents = mod.infrastructure_documents
409
+ return if documents.empty?
410
+
411
+ infrastructure = File.join(root, "infrastructure")
412
+ used = {}
413
+ paths = documents.map do |document|
414
+ base = underscore(document.fetch(:name))
415
+ relative = unique_relative_path(document.fetch(:route), base, used)
416
+ write(File.join(infrastructure, relative), mapping_document_source(document))
417
+ relative
418
+ end
419
+ append_to_aggregator(File.join(infrastructure, "infrastructure.rb"), paths)
420
+ end
421
+
422
+ def export_application_documents(root, mod)
423
+ documents = mod.application_documents
424
+ return if documents.empty?
425
+
426
+ application = File.join(root, 'application')
427
+ used = {}
428
+ paths = documents.map do |document|
429
+ relative = unique_relative_path(document.fetch(:route), underscore(document.fetch(:name)), used)
430
+ write(File.join(application, relative), mapping_document_source(document))
431
+ relative
432
+ end
433
+ append_to_aggregator(File.join(application, 'application.rb'), paths)
434
+ end
435
+
436
+ def unique_relative_path(directory, base, used)
437
+ candidate = File.join(directory, "#{base}.rb")
438
+ suffix = 2
439
+ while used[candidate]
440
+ candidate = File.join(directory, "#{base}_#{suffix}.rb")
441
+ suffix += 1
442
+ end
443
+ used[candidate] = true
444
+ candidate
445
+ end
446
+
447
+ def mapping_document_source(document)
448
+ <<~RUBY
449
+ # frozen_string_literal: true
450
+
451
+ #{native_document_declaration(document)}
452
+ RUBY
453
+ end
454
+
455
+ def native_document_declaration(document)
456
+ <<~RUBY.rstrip
457
+ native_document #{symbol(document.fetch(:name))},
458
+ type: #{ruby(document.fetch(:type))},
459
+ unit_id: #{ruby(document.fetch(:id))},
460
+ container_id: #{ruby(document.fetch(:container_id))},
461
+ containment: #{ruby(document.fetch(:containment))},
462
+ deep_structure: #{native_ruby(document.fetch(:doc), 24)}
463
+ RUBY
464
+ end
465
+
466
+ def append_to_aggregator(path, relative_paths)
467
+ existing = File.exist?(path) ? File.read(path).lines : []
468
+ additions = relative_paths.sort.map do |relative|
469
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
470
+ "evaluate File.join(__dir__, #{segments})\n"
471
+ end
472
+ write(path, (existing + additions).uniq.join)
473
+ end
474
+
299
475
  def export_menus(root, mod)
300
476
  menus = mod.menus
301
477
  return if menus.empty?
@@ -326,7 +502,10 @@ module Mxrb
326
502
  architecture = architecture_module(mod.name)
327
503
  flows.each do |flow|
328
504
  relative = File.join("client_actions", files.fetch(flow.id))
329
- metadata = architecture&.fetch(:nanoflows, [])&.find { _1[:name] == flow.name }
505
+ metadata = flow_metadata(
506
+ mod.name, :nanoflow, flow.name,
507
+ architecture&.fetch(:nanoflows, [])&.find { _1[:name] == flow.name }
508
+ )
330
509
  write(File.join(presentation, relative), nanoflow_source(flow, metadata))
331
510
  paths << relative
332
511
  end
@@ -357,7 +536,7 @@ module Mxrb
357
536
  end
358
537
  end
359
538
 
360
- def write_project(project)
539
+ def write_project(project, ruby_sources: false)
361
540
  module_loads = project.modules.map do |mod|
362
541
  %( evaluate File.join(__dir__, "modules", #{ruby(mod.name)}, "module.rb"))
363
542
  end.join("\n")
@@ -373,6 +552,7 @@ module Mxrb
373
552
  mendix_version #{ruby(project.mendix_version || "10.18.0")}
374
553
  native_units File.join(__dir__, ".mxrb", "native_units.json")
375
554
  project_assets File.join(__dir__, ".mxrb", "assets.json"), root: __dir__
555
+ #{' ruby_app_sources File.join(__dir__, ".mxrb", "ruby_sources.json")' if ruby_sources}
376
556
  evaluate File.join(__dir__, ".mxrb", "native_units.rb")
377
557
  evaluate File.join(__dir__, "app", "security", "security.rb")
378
558
  evaluate File.join(__dir__, "app", "navigation", "navigation.rb")
@@ -586,6 +766,12 @@ module Mxrb
586
766
  end
587
767
  flags = []
588
768
  flags << " non_persistent!" unless entity.persistable
769
+ if oql_view_entity?(entity)
770
+ options = []
771
+ options << "source: #{ruby(entity.oql_source_document)}" if entity.oql_source_document
772
+ options << "query: #{ruby(entity.oql_query)}" unless entity.oql_query.to_s.empty?
773
+ flags << " oql_view #{options.join(', ')}" unless options.empty?
774
+ end
589
775
  flags << " documentation #{ruby(entity.documentation)}" unless entity.documentation.to_s.empty?
590
776
  generalization = entity.respond_to?(:generalization_target) ? entity.generalization_target : nil
591
777
  flags << " generalizes #{ruby(generalization)}" if generalization
@@ -678,19 +864,21 @@ module Mxrb
678
864
  end
679
865
  end
680
866
 
681
- def unique_entity_filenames(entities)
682
- unique_filenames(entities)
867
+ def unique_entity_filenames(entities, route: nil)
868
+ suffix = route == "dtos" ? "_dto" : nil
869
+ unique_filenames(entities, suffix:)
683
870
  end
684
871
 
685
- def unique_filenames(entities)
872
+ def unique_filenames(entities, suffix: nil)
686
873
  used = {}
687
874
  entities.to_h do |entity|
688
875
  base = underscore(entity.name)
876
+ base = "#{base}#{suffix}" if suffix && !base.end_with?(suffix)
689
877
  candidate = "#{base}.rb"
690
- suffix = 2
878
+ sequence = 2
691
879
  while used[candidate]
692
- candidate = "#{base}_#{suffix}.rb"
693
- suffix += 1
880
+ candidate = "#{base}_#{sequence}.rb"
881
+ sequence += 1
694
882
  end
695
883
  used[candidate] = true
696
884
  [entity.id, candidate]
@@ -718,6 +906,8 @@ module Mxrb
718
906
  body << " return_type #{symbol(flow.return_type)}" if flow.return_type.is_a?(String)
719
907
  body << " documentation #{ruby(flow.documentation)}" unless flow.documentation.to_s.empty?
720
908
  body << " allow_concurrent_execution #{flow.allow_concurrent_execution ? 'true' : 'false'}"
909
+ apply_entity_access = flow.respond_to?(:apply_entity_access) && flow.apply_entity_access
910
+ body << " apply_entity_access #{apply_entity_access ? 'true' : 'false'}"
721
911
  body << " mark_as_used #{flow.mark_as_used ? 'true' : 'false'}"
722
912
  body << " excluded #{flow.excluded ? 'true' : 'false'}"
723
913
  body << " allowed_roles #{flow.allowed_module_roles.map { symbol(_1) }.join(', ')}" unless flow.allowed_module_roles.empty?
@@ -737,10 +927,12 @@ module Mxrb
737
927
  elsif objects.any?
738
928
  body << " # Native body baseline retained: this graph shape has no typed DSL mapping yet"
739
929
  end
930
+ declaration = [symbol(flow.name)]
931
+ declaration << "public: true" if metadata&.fetch(:public, false)
740
932
  <<~RUBY
741
933
  # frozen_string_literal: true
742
934
 
743
- microflow #{symbol(flow.name)} do
935
+ microflow #{declaration.join(', ')} do
744
936
  #{body.join("\n")}
745
937
  end
746
938
  RUBY
@@ -781,7 +973,7 @@ module Mxrb
781
973
  body << " allowed_roles #{page.allowed_module_roles.map { symbol(_1) }.join(', ')}" unless page.allowed_module_roles.empty?
782
974
  deep = page_deep_structure(page)
783
975
  if (source = metadata&.dig(:data_source) || page.data_source)
784
- body << " data_source #{source.fetch(:kind)}: #{symbol(source.fetch(:name))}"
976
+ body << " data_source #{source.fetch(:kind)}: #{reference(source.fetch(:name))}"
785
977
  end
786
978
  body << " deep_structure(#{native_ruby(deep, 2)})" if deep
787
979
  metadata&.fetch(:events, [])&.each do |event|
@@ -870,7 +1062,9 @@ module Mxrb
870
1062
  def render_widget(widget, indent)
871
1063
  pad = " " * indent
872
1064
  child_pad = " " * (indent + 2)
873
- type = widget.fetch(:type)
1065
+ # Architecture metadata is serialized through JSON inside generated MPRs,
1066
+ # so a second export can surface widget type values as strings.
1067
+ type = widget.fetch(:type).to_sym
874
1068
  options = widget.fetch(:options, {})
875
1069
 
876
1070
  # Snippet: snippet :name, from: "..."
@@ -1710,6 +1904,32 @@ module Mxrb
1710
1904
  @architecture&.fetch(:modules, [])&.find { _1[:name] == name }
1711
1905
  end
1712
1906
 
1907
+ def infer_public_artifacts(modules)
1908
+ modules.each_with_object({}) do |mod, public_artifacts|
1909
+ mod.pages.each do |page|
1910
+ source = page.data_source
1911
+ next unless source
1912
+
1913
+ target_module, target_name = source.fetch(:name).to_s.split(".", 2)
1914
+ next if target_name.to_s.empty? || target_module == mod.name
1915
+
1916
+ public_artifacts[[target_module, source.fetch(:kind).to_sym, target_name]] = true
1917
+ end
1918
+ end
1919
+ end
1920
+
1921
+ def flow_metadata(module_name, kind, name, metadata)
1922
+ result = metadata ? metadata.dup : {}
1923
+ if @inferred_public_artifacts&.include?([module_name, kind, name])
1924
+ result[:public] = true
1925
+ end
1926
+ result.empty? ? nil : result
1927
+ end
1928
+
1929
+ def reference(value)
1930
+ value.to_s.include?(".") ? ruby(value.to_s) : symbol(value)
1931
+ end
1932
+
1713
1933
  def symbol(value)
1714
1934
  value.to_s.to_sym.inspect
1715
1935
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puma'
4
+ require 'uri'
5
+
6
+ module Mxrb
7
+ module Http
8
+ # Case-insensitive HTTP header collection used by the internal request API.
9
+ class Headers
10
+ def initialize(values = {})
11
+ @values = values.to_h { |name, value| [name.to_s.downcase, value.to_s] }
12
+ end
13
+
14
+ def [](name) = @values[name.to_s.downcase]
15
+ end
16
+
17
+ Request = Data.define(:path, :request_method, :body, :query, :headers) do
18
+ def [](name) = headers[name]
19
+ end
20
+
21
+ # Mutable response keeps the existing MXRB dispatchers independent of Rack.
22
+ class Response
23
+ attr_accessor :status, :body
24
+ attr_reader :headers
25
+
26
+ def initialize
27
+ @status = 200
28
+ @body = ''
29
+ @headers = {}
30
+ end
31
+
32
+ def [](name) = headers[name]
33
+
34
+ def []=(name, value)
35
+ headers[name] = value
36
+ end
37
+ end
38
+
39
+ # Small embedded-Puma adapter for MXRB's loopback-only HTTP services.
40
+ class Server
41
+ DEFAULT_MAX_THREADS = 5
42
+
43
+ attr_reader :host, :port, :puma
44
+
45
+ def initialize(host:, port:, logger: nil, max_threads: DEFAULT_MAX_THREADS, &dispatcher)
46
+ raise ArgumentError, 'HTTP dispatcher is required' unless dispatcher
47
+
48
+ @host = host.to_s
49
+ @port = Integer(port)
50
+ @dispatcher = dispatcher
51
+ @logger = logger
52
+ @max_threads = Integer(max_threads)
53
+ end
54
+
55
+ def start
56
+ @puma = Puma::Server.new(method(:call), nil, puma_options)
57
+ @puma.add_tcp_listener(host, port)
58
+ yield(@puma) if block_given?
59
+ @thread = @puma.run
60
+ @thread.join
61
+ end
62
+
63
+ def shutdown = @puma&.stop
64
+
65
+ def call(environment)
66
+ request = request_from(environment)
67
+ response = Response.new
68
+ @dispatcher.call(request, response)
69
+ [response.status, response.headers, [response.body.to_s]]
70
+ end
71
+
72
+ private
73
+
74
+ def puma_options
75
+ {
76
+ min_threads: 0,
77
+ max_threads: @max_threads,
78
+ environment: 'production',
79
+ log_writer: @logger || Puma::LogWriter.null,
80
+ queue_requests: true
81
+ }
82
+ end
83
+
84
+ def request_from(environment)
85
+ input = environment['rack.input']
86
+ body = input ? input.read.to_s : ''
87
+ input.rewind if input.respond_to?(:rewind)
88
+ Request.new(
89
+ path: "#{environment['SCRIPT_NAME']}#{environment.fetch('PATH_INFO', '/')}",
90
+ request_method: environment.fetch('REQUEST_METHOD', 'GET'),
91
+ body:,
92
+ query: URI.decode_www_form(environment['QUERY_STRING'].to_s).to_h,
93
+ headers: Headers.new(request_headers(environment))
94
+ )
95
+ end
96
+
97
+ def request_headers(environment)
98
+ environment.each_with_object({}) do |(name, value), headers|
99
+ next unless name.start_with?('HTTP_')
100
+
101
+ header = name.delete_prefix('HTTP_').split('_').map(&:capitalize).join('-')
102
+ headers[header] = value
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end