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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ module Mxrb
7
+ # Creates the minimum editable Ruby project accepted by `mxrb generate`.
8
+ # rubocop:disable Metrics/ClassLength
9
+ class Initializer
10
+ Result = Data.define(:root, :files)
11
+ NAME = /\A[A-Za-z][A-Za-z0-9_-]*\z/
12
+ COMPOUND_SUFFIXES = %w[service clinic portal demo api app kit].freeze
13
+
14
+ def initialize(name, subject: :project, mxrb_path: nil)
15
+ @dir_name = name.to_s
16
+ raise ArgumentError, "#{subject} name must be snake_case or PascalCase" unless NAME.match?(@dir_name)
17
+
18
+ @module_name = to_pascal_case(@dir_name)
19
+ @mpr_name = "#{@module_name}.mpr"
20
+ @mxrb_path = File.expand_path(mxrb_path) if mxrb_path
21
+ raise ArgumentError, "mxrb path does not exist: #{@mxrb_path}" if @mxrb_path && !File.directory?(@mxrb_path)
22
+ end
23
+
24
+ def scaffold(into: Dir.pwd)
25
+ parent = File.expand_path(into)
26
+ root = File.join(parent, @dir_name)
27
+ abort "#{root}: directory already exists" if File.exist?(root)
28
+
29
+ FileUtils.mkdir_p(parent)
30
+ staging = Dir.mktmpdir(".#{@dir_name}.mxrb-init-", parent)
31
+ write_scaffold(staging)
32
+ FileUtils.mv(staging, root)
33
+ Result.new(root, relative_files.map { File.join(root, _1) }.freeze)
34
+ ensure
35
+ FileUtils.rm_rf(staging) if staging && File.exist?(staging)
36
+ end
37
+
38
+ private
39
+
40
+ def write_scaffold(root)
41
+ write(root, 'Gemfile', gemfile)
42
+ write(root, '.gitignore', gitignore)
43
+ write(root, '.env.example', env_example)
44
+ write(root, 'project.rb', project_rb)
45
+ write(File.join(root, 'app', 'navigation', 'responsive'), '.keep', '')
46
+ write_design_scaffold(root)
47
+ module_root = File.join(root, 'modules', @module_name)
48
+ write_module_scaffold(module_root)
49
+ end
50
+
51
+ def write_design_scaffold(root)
52
+ design_files.each do |parts, template|
53
+ write(File.join(root, *parts[0...-1]), parts.last, Scaffold::Templates.render(template))
54
+ end
55
+ end
56
+
57
+ def design_files
58
+ {
59
+ %w[app design_system design_system.rb] => :design_system,
60
+ %w[theme web custom-variables.scss] => :theme_custom_variables,
61
+ %w[theme web main.scss] => :theme_main,
62
+ %w[theme web exclusion-variables.scss] => :theme_exclusion_variables,
63
+ %w[theme web settings.json] => :theme_settings,
64
+ %w[theme-cache web theme.compiled.css] => :theme_compiled
65
+ }
66
+ end
67
+
68
+ def write_module_scaffold(module_root)
69
+ write(module_root, 'module.rb', module_rb)
70
+ write(File.join(module_root, 'domain'), 'model.rb', model_rb)
71
+ write(File.join(module_root, 'application'), 'application.rb', application_rb)
72
+ write(File.join(module_root, 'domain', 'entities'), '.keep', '')
73
+ write(File.join(module_root, 'presentation'), 'presentation.rb', presentation_rb)
74
+ write(File.join(module_root, 'presentation', 'pages'), 'home.rb', home_rb)
75
+ end
76
+
77
+ def relative_files
78
+ module_prefix = File.join('modules', @module_name)
79
+ ['Gemfile', '.gitignore', '.env.example', 'project.rb',
80
+ *relative_module_files.map { File.join(module_prefix, _1) },
81
+ File.join('app', 'navigation', 'responsive', '.keep'),
82
+ *design_files.keys.map { File.join(*_1) }]
83
+ end
84
+
85
+ def relative_module_files
86
+ [
87
+ 'module.rb', File.join('domain', 'model.rb'),
88
+ File.join('application', 'application.rb'), File.join('domain', 'entities', '.keep'),
89
+ File.join('presentation', 'presentation.rb'), File.join('presentation', 'pages', 'home.rb')
90
+ ]
91
+ end
92
+
93
+ def to_pascal_case(value)
94
+ parts = value.split(/[_-]+/)
95
+ return parts.map { capitalize(_1) }.join if parts.length > 1
96
+ return capitalize(value) unless value == value.downcase
97
+
98
+ stem = value.sub(/\d+\z/, '')
99
+ digits = value.delete_prefix(stem)
100
+ compound_parts(stem).map { capitalize(_1) }.join + digits
101
+ end
102
+
103
+ def compound_parts(value)
104
+ suffix = COMPOUND_SUFFIXES.find { value.length > _1.length && value.end_with?(_1) }
105
+ return [value] unless suffix
106
+
107
+ [*compound_parts(value.delete_suffix(suffix)), suffix]
108
+ end
109
+
110
+ def capitalize(value) = value[0].upcase + value[1..]
111
+
112
+ def write(directory, name, content)
113
+ FileUtils.mkdir_p(directory)
114
+ File.binwrite(File.join(directory, name), content)
115
+ end
116
+
117
+ def gemfile
118
+ <<~RUBY
119
+ # frozen_string_literal: true
120
+
121
+ source "https://rubygems.org"
122
+
123
+ #{gem_declaration}
124
+ gem "dotenv", "~> 3.0"
125
+ RUBY
126
+ end
127
+
128
+ def gitignore
129
+ <<~TEXT
130
+ # Local environment and secrets. Commit only .env.example with blank values.
131
+ .env
132
+ .env.*
133
+ !.env.example
134
+ TEXT
135
+ end
136
+
137
+ def env_example
138
+ <<~TEXT
139
+ # Copy to .env and keep real values local. Never commit secrets.
140
+ MXRB_MENDIX_PAT=
141
+ TEXT
142
+ end
143
+
144
+ def gem_declaration
145
+ return 'gem "mxrb"' unless @mxrb_path
146
+
147
+ %(gem "mxrb", path: #{@mxrb_path.inspect})
148
+ end
149
+
150
+ def project_rb
151
+ <<~RUBY
152
+ # frozen_string_literal: true
153
+
154
+ require "dotenv/load"
155
+ require "mxrb"
156
+
157
+ output = ENV.fetch("MXRB_OUTPUT_PATH", File.join(__dir__, "#{@mpr_name}"))
158
+
159
+ Mxrb.define(output) do
160
+ mendix_version "11.12.1"
161
+
162
+ navigation do
163
+ profile :Responsive,
164
+ home_page: "#{@module_name}.Home",
165
+ app_title: "#{@module_name}" do
166
+ evaluate_dir File.join(__dir__, "app", "navigation", "responsive")
167
+ end
168
+ end
169
+
170
+ evaluate File.join(__dir__, "app", "design_system", "design_system.rb")
171
+ evaluate File.join(__dir__, "modules", "#{@module_name}", "module.rb")
172
+ end
173
+ RUBY
174
+ end
175
+
176
+ def module_rb
177
+ <<~RUBY
178
+ # frozen_string_literal: true
179
+
180
+ self.module :#{@module_name} do
181
+ evaluate File.join(__dir__, "domain", "model.rb")
182
+ evaluate File.join(__dir__, "application", "application.rb")
183
+ evaluate File.join(__dir__, "presentation", "presentation.rb")
184
+ end
185
+ RUBY
186
+ end
187
+
188
+ def model_rb
189
+ <<~RUBY
190
+ # frozen_string_literal: true
191
+
192
+ evaluate_dir File.join(__dir__, "enumerations")
193
+ evaluate_dir File.join(__dir__, "entities")
194
+ RUBY
195
+ end
196
+
197
+ def application_rb
198
+ <<~RUBY
199
+ # frozen_string_literal: true
200
+
201
+ evaluate_dir File.join(__dir__, "use_cases")
202
+ evaluate_dir File.join(__dir__, "validations")
203
+ evaluate_dir File.join(__dir__, "queries")
204
+ RUBY
205
+ end
206
+
207
+ def presentation_rb
208
+ <<~RUBY
209
+ # frozen_string_literal: true
210
+
211
+ layout :ApplicationLayout, title: "#{@module_name}"
212
+
213
+ evaluate_dir File.join(__dir__, "pages")
214
+ evaluate_dir File.join(__dir__, "snippets")
215
+ evaluate_dir File.join(__dir__, "client_actions")
216
+ RUBY
217
+ end
218
+
219
+ def home_rb
220
+ <<~RUBY
221
+ # frozen_string_literal: true
222
+
223
+ page :Home do
224
+ title "#{@module_name}"
225
+ layout "#{@module_name}.ApplicationLayout"
226
+
227
+ container :pageHeader, class_name: "mxrb-page-header" do
228
+ text :pageTitle, caption: "#{@module_name}"
229
+ text :pageSubtitle,
230
+ caption: "A Ruby-first Mendix application, ready to grow."
231
+ end
232
+
233
+ container :dashboard, class_name: "mxrb-dashboard-grid" do
234
+ container :domainCard, class_name: "mxrb-card" do
235
+ text :domainEyebrow, caption: "DOMAIN"
236
+ text :domainTitle, caption: "Model your business"
237
+ text :domainCopy, caption: "Add entities, enumerations and associations using the mxrb DSL."
238
+ end
239
+
240
+ container :logicCard, class_name: "mxrb-card" do
241
+ text :logicEyebrow, caption: "LOGIC"
242
+ text :logicTitle, caption: "Build application flows"
243
+ text :logicCopy, caption: "Keep use cases, validations and queries organized by convention."
244
+ end
245
+
246
+ container :experienceCard, class_name: "mxrb-card" do
247
+ text :experienceEyebrow, caption: "EXPERIENCE"
248
+ text :experienceTitle, caption: "Ship a polished UI"
249
+ text :experienceCopy, caption: "Create pages, snippets and client actions without manual wiring."
250
+ end
251
+ end
252
+ end
253
+ RUBY
254
+ end
255
+ end
256
+ # rubocop:enable Metrics/ClassLength
257
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Mxrb
6
+ module Integrity
7
+ Result = Struct.new(:errors, :warnings, keyword_init: true) do
8
+ def valid? = errors.empty?
9
+ end
10
+
11
+ class Validator
12
+ def initialize(path)
13
+ @path = path
14
+ end
15
+
16
+ def validate
17
+ @errors = []
18
+ @warnings = []
19
+ @mpr = IO::MprFile.open(@path, readonly: true)
20
+ validate_tables
21
+ validate_units
22
+ validate_v2_files
23
+ Result.new(errors: @errors, warnings: @warnings)
24
+ rescue Error => e
25
+ Result.new(errors: [e.message], warnings: @warnings || [])
26
+ ensure
27
+ @mpr&.close
28
+ end
29
+
30
+ private
31
+
32
+ def validate_tables
33
+ require_table("Unit")
34
+ require_table("_MetaData")
35
+ require_unit_column("UnitID")
36
+ require_unit_column("ContainerID")
37
+ require_unit_column("ContainmentName")
38
+ require_unit_column("ContentsHash")
39
+ end
40
+
41
+ def validate_units
42
+ @units = @mpr.all_units
43
+ validate_root
44
+ validate_unit_ids
45
+ validate_contents
46
+ end
47
+
48
+ def validate_root
49
+ roots = @units.select { _1["UnitID"] == _1["ContainerID"] }
50
+ add_error("expected exactly one root unit, found #{roots.size}") unless roots.size == 1
51
+ end
52
+
53
+ def validate_unit_ids
54
+ ids = @units.map { _1["UnitID"] }
55
+ ids.each { add_error("blank UnitID") if _1.to_s.empty? }
56
+ duplicates = ids.tally.select { |_id, count| count > 1 }.keys
57
+ duplicates.each { add_error("duplicate UnitID #{_1}") }
58
+
59
+ id_set = ids.to_set
60
+ @units.each do |unit|
61
+ parent = unit["ContainerID"]
62
+ next if parent == unit["UnitID"]
63
+
64
+ add_error("unit #{unit['UnitID']} references missing container #{parent}") unless id_set.include?(parent)
65
+ end
66
+ end
67
+
68
+ def validate_contents
69
+ @units.each do |unit|
70
+ bytes = @mpr.content_bytes(unit)
71
+ if bytes.nil? || bytes.empty?
72
+ add_error("unit #{unit['UnitID']} has no content bytes")
73
+ next
74
+ end
75
+
76
+ validate_hash(unit, bytes)
77
+ doc = parse_unit(unit)
78
+ next unless doc
79
+
80
+ validate_doc_identity(unit, doc)
81
+ end
82
+ end
83
+
84
+ def validate_hash(unit, bytes)
85
+ expected = unit["ContentsHash"].to_s
86
+ actual = IO::BsonCodec.contents_hash(bytes)
87
+ add_error("unit #{unit['UnitID']} ContentsHash mismatch") unless expected == actual
88
+ end
89
+
90
+ def parse_unit(unit)
91
+ @mpr.parse_contents(unit)
92
+ rescue IO::MxunitCodec::UnsupportedFormat, SerializationError => e
93
+ add_error("unit #{unit['UnitID']} content parse failed: #{e.message}")
94
+ nil
95
+ end
96
+
97
+ def validate_doc_identity(unit, doc)
98
+ type = doc["$Type"] || doc["\$Type"]
99
+ add_error("unit #{unit['UnitID']} missing $Type") if type.to_s.empty?
100
+
101
+ doc_id = IO::BsonCodec.extract_id(doc["$ID"] || doc["\$ID"])
102
+ if doc_id.to_s.empty?
103
+ add_error("unit #{unit['UnitID']} missing $ID")
104
+ elsif doc_id != unit["UnitID"]
105
+ add_error("unit #{unit['UnitID']} content $ID mismatch #{doc_id}")
106
+ end
107
+ end
108
+
109
+ def validate_v2_files
110
+ return unless @mpr.format_version == :v2
111
+
112
+ expected = @units.filter_map { @mpr.content_path(_1) }.to_set
113
+ actual = @mpr.content_files.to_set
114
+ (expected - actual).each { add_error("missing mxunit file #{_1}") }
115
+ (actual - expected).each { add_warning("orphan mxunit file #{_1}") }
116
+ end
117
+
118
+ def require_table(name)
119
+ add_error("missing #{name} table") unless @mpr.tables.include?(name)
120
+ end
121
+
122
+ def require_unit_column(name)
123
+ columns = @mpr.table_info("Unit").map { _1["name"] || _1[:name] }
124
+ add_error("missing Unit.#{name} column") unless columns.include?(name)
125
+ end
126
+
127
+ def add_error(message)
128
+ @errors << message
129
+ end
130
+
131
+ def add_warning(message)
132
+ @warnings << message
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bson"
4
+ require "digest"
5
+ require "base64"
6
+
7
+ module Mxrb
8
+ module IO
9
+ # BSON codec with Mendix-specific conventions:
10
+ # - UUIDs stored as 16-byte blobs with partial little-endian byte-swap (MS GUID format)
11
+ # - Arrays have a 4-byte int32 marker as first element (1=by-name, 2=part-secondary, 3=part-primary)
12
+ # - $ID can appear as String, BSON Binary, or {Data:, Subtype:} map
13
+ # - $Type uses storage names ("DomainModels$Entity") not SDK qualified names
14
+ module BsonCodec
15
+ # ── UUID / BLOB conversion ─────────────────────────────────────────────
16
+
17
+ # Convert a 16-byte MS-GUID blob to UUID string.
18
+ # MS-GUID layout: bytes 0-3 little-endian, 4-5 little-endian, 6-7 little-endian,
19
+ # bytes 8-15 big-endian.
20
+ def self.blob_to_uuid(blob)
21
+ return nil unless blob.is_a?(String) && blob.bytesize == 16
22
+
23
+ b = blob.bytes
24
+ p1 = b[0..3].reverse.map { "%02x" % _1 }.join
25
+ p2 = b[4..5].reverse.map { "%02x" % _1 }.join
26
+ p3 = b[6..7].reverse.map { "%02x" % _1 }.join
27
+ p4 = b[8..9].map { "%02x" % _1 }.join
28
+ p5 = b[10..15].map { "%02x" % _1 }.join
29
+ "#{p1}-#{p2}-#{p3}-#{p4}-#{p5}"
30
+ end
31
+
32
+ # Convert a UUID string to a 16-byte MS-GUID blob.
33
+ def self.uuid_to_blob(uuid)
34
+ hex = uuid.delete("-")
35
+ parts = hex.scan(/../).map { _1.to_i(16).chr(Encoding::BINARY) }
36
+ (parts[0..3].reverse + parts[4..5].reverse + parts[6..7].reverse + parts[8..15]).join
37
+ end
38
+
39
+ # ── $ID extraction ─────────────────────────────────────────────────────
40
+
41
+ # Extract the Mendix $ID value from any of its three representations:
42
+ # 1. String UUID: "c67c5271-da7d-45f1-81df-ceb6946b8abe"
43
+ # 2. BSON Binary: <16-byte blob> → converted to UUID string
44
+ # 3. Map: { "Data" => "base64...", "Subtype" => 3 } → decoded + converted
45
+ def self.extract_id(value)
46
+ case value
47
+ when String
48
+ value # already a UUID string
49
+ when BSON::Binary
50
+ blob_to_uuid(value.data)
51
+ when Hash
52
+ data = value["Data"] || value[:Data]
53
+ return nil unless data
54
+ blob_to_uuid(Base64.strict_decode64(data.to_s))
55
+ end
56
+ end
57
+
58
+ # ── Array parsing ───────────────────────────────────────────────────────
59
+
60
+ # Mendix arrays have an int32 marker as first element.
61
+ # Returns { marker: Integer, items: Array }
62
+ # Marker values: 1=by-name refs, 2=part-secondary, 3=part-primary
63
+ def self.parse_array(raw)
64
+ return { marker: 3, items: [] } if raw.nil? || raw.empty?
65
+
66
+ marker = raw.first.is_a?(Integer) ? raw.first : 3
67
+ items = raw.first.is_a?(Integer) ? raw[1..] : raw
68
+ { marker: marker, items: Array(items) }
69
+ end
70
+
71
+ # Build a Mendix-style array (prepend marker).
72
+ def self.build_array(items, marker: 3)
73
+ [marker] + Array(items)
74
+ end
75
+
76
+ # ── BSON parse / serialize ──────────────────────────────────────────────
77
+
78
+ # Parse raw BSON bytes → Ruby Hash (with string keys).
79
+ def self.parse(bytes)
80
+ return {} if bytes.nil? || bytes.empty?
81
+
82
+ buf = BSON::ByteBuffer.new(bytes.b)
83
+ BSON::Document.from_bson(buf).to_h
84
+ rescue => e
85
+ raise SerializationError, "BSON parse error: #{e.message}"
86
+ end
87
+
88
+ # Serialize a Ruby Hash → BSON bytes.
89
+ def self.serialize(doc)
90
+ bson_doc = BSON::Document.new(storage_value(doc))
91
+ buf = BSON::ByteBuffer.new
92
+ bson_doc.to_bson(buf)
93
+ buf.get_bytes(buf.length)
94
+ rescue => e
95
+ raise SerializationError, "BSON serialize error: #{e.message}"
96
+ end
97
+
98
+ UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
99
+ BINARY_UUID_KEYS = %w[
100
+ $ID ChildPointer CloseButtonPointer DataStorageGuid DefaultButtonPointer
101
+ DefaultPagePointer DestinationPointer GUID OriginPointer ParentPointer
102
+ StableId TypeParameterPointer TypePointer
103
+ ].freeze
104
+
105
+ def self.storage_value(value, key = nil)
106
+ if BINARY_UUID_KEYS.include?(key) &&
107
+ value.is_a?(String) && value.match?(UUID_PATTERN)
108
+ return BSON::Binary.new(uuid_to_blob(value), :generic)
109
+ end
110
+
111
+ case value
112
+ when Hash
113
+ value.to_h { |child_key, child| [child_key, storage_value(child, child_key.to_s)] }
114
+ when Array
115
+ value.map { storage_value(_1) }
116
+ else
117
+ value
118
+ end
119
+ end
120
+ private_class_method :storage_value
121
+
122
+ # ── ContentHash ────────────────────────────────────────────────────────
123
+
124
+ # Compute the Mendix ContentsHash: Base64(SHA256(bson_bytes)).
125
+ def self.contents_hash(bson_bytes)
126
+ Base64.strict_encode64(Digest::SHA256.digest(bson_bytes.b))
127
+ end
128
+
129
+ # ── Storage name helpers ───────────────────────────────────────────────
130
+
131
+ # Map ContainmentName → expected $Type prefix for quick type detection.
132
+ CONTAINMENT_TO_TYPE = {
133
+ "Modules" => "Projects$Module",
134
+ "DomainModel" => "DomainModels$DomainModel",
135
+ "Documents" => nil, # could be Page, Microflow, etc — read $Type
136
+ "Folders" => "Projects$Folder",
137
+ "ModuleSecurity" => "Security$ModuleSecurity",
138
+ "Settings" => "Settings$ProjectSettings",
139
+ "Security" => "Security$ProjectSecurity",
140
+ "NavigationDocuments" => "Navigation$NavigationDocument",
141
+ "SystemTexts" => "Texts$SystemTextCollection",
142
+ }.freeze
143
+ end
144
+ end
145
+ end