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,380 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "json"
6
+ require "net/http"
7
+ require "open3"
8
+ require "pathname"
9
+ require "tmpdir"
10
+ require "uri"
11
+
12
+ module Mxrb
13
+ module Marketplace
14
+ Entry = Data.define(:name, :version, :description, :source, :ref)
15
+ Installation = Data.define(:entry, :module_name, :destination, :digest)
16
+
17
+ class Catalog
18
+ DEFAULT_PATH = File.expand_path("../../marketplace/catalog.json", __dir__)
19
+
20
+ def initialize(source = nil)
21
+ @source = source || DEFAULT_PATH
22
+ end
23
+
24
+ def entries
25
+ @entries ||= begin
26
+ payload = JSON.parse(read_source)
27
+ Array(payload.fetch("modules")).map do |item|
28
+ Entry.new(
29
+ item.fetch("name"), item.fetch("version"),
30
+ item.fetch("description", ""), item.fetch("source"),
31
+ item["ref"]
32
+ )
33
+ end.freeze
34
+ rescue JSON::ParserError, KeyError => e
35
+ raise MarketplaceError, "invalid marketplace catalog: #{e.message}"
36
+ end
37
+ end
38
+
39
+ def search(query = nil)
40
+ term = query.to_s.downcase
41
+ return entries if term.empty?
42
+
43
+ entries.select do |entry|
44
+ [entry.name, entry.description].any? { _1.downcase.include?(term) }
45
+ end
46
+ end
47
+
48
+ def find(name)
49
+ entries.find { _1.name == name.to_s } ||
50
+ raise(MarketplaceError, "module #{name.inspect} was not found in the catalog")
51
+ end
52
+
53
+ def find_version(name, version)
54
+ entries.find { _1.name == name.to_s && _1.version == version.to_s } ||
55
+ raise(MarketplaceError, "module #{name.inspect} version #{version.inspect} was not found in the catalog")
56
+ end
57
+
58
+ private
59
+
60
+ def read_source
61
+ uri = URI.parse(@source.to_s)
62
+ return File.read(@source) unless uri.is_a?(URI::HTTP)
63
+ raise MarketplaceError, "marketplace catalogs must use HTTPS" unless uri.is_a?(URI::HTTPS)
64
+
65
+ response = Net::HTTP.get_response(uri)
66
+ raise MarketplaceError, "catalog request failed with HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
67
+
68
+ response.body
69
+ rescue URI::InvalidURIError, Errno::ENOENT => e
70
+ raise MarketplaceError, "cannot read marketplace catalog: #{e.message}"
71
+ end
72
+ end
73
+
74
+ class Installer
75
+ MANIFEST = "mxrb-module.json"
76
+
77
+ def initialize(target:, catalog: Catalog.new)
78
+ @target = File.expand_path(target)
79
+ @catalog = catalog
80
+ end
81
+
82
+ def install(identifier, version: nil)
83
+ install_package(identifier, version: version, replace: false)
84
+ end
85
+
86
+ def update(identifier, version: nil)
87
+ installed_module_name = resolve_installed_module_name(identifier)
88
+ install_package(
89
+ identifier,
90
+ version: version,
91
+ replace: true,
92
+ installed_module_name: installed_module_name
93
+ )
94
+ end
95
+
96
+ def remove(identifier)
97
+ module_name = resolve_installed_module_name(identifier)
98
+ validate_no_dependents!(module_name)
99
+ destination = File.join(@target, "modules", module_name)
100
+ FileUtils.rm_rf(destination) if File.exist?(destination)
101
+ remove_from_lock(module_name)
102
+ module_name
103
+ end
104
+
105
+ private
106
+
107
+ def install_package(identifier, version:, replace:, installed_module_name: nil)
108
+ entry, source = resolve(identifier, version: version)
109
+ Dir.mktmpdir("mxrb-module-") do |workspace|
110
+ package = materialize(source, entry, workspace)
111
+ manifest = load_manifest(package)
112
+ module_name = valid_name(manifest.fetch("module_name"))
113
+ destination = File.join(@target, "modules", module_name)
114
+ if installed_module_name && module_name != installed_module_name
115
+ raise MarketplaceError,
116
+ "module update cannot change its name from #{installed_module_name.inspect} to #{module_name.inspect}"
117
+ end
118
+ if File.exist?(destination) && !replace
119
+ raise MarketplaceError, "module destination already exists: #{destination}"
120
+ end
121
+
122
+ manifest_name = manifest["name"].to_s
123
+ canonical_entry = manifest_name.empty? ? entry : Entry.new(
124
+ manifest_name, entry.version, entry.description, entry.source, entry.ref
125
+ )
126
+ validate_mendix_version!(manifest)
127
+ validate_dependencies!(manifest)
128
+ files = package_files(package, manifest)
129
+ staging = File.join(@target, ".mxrb", "staging", "#{module_name}-#{Process.pid}")
130
+ backup = nil
131
+ FileUtils.mkdir_p(staging)
132
+ files.each { copy_entry(package, staging, _1) }
133
+ if replace && File.exist?(destination)
134
+ backup = File.join(@target, ".mxrb", "backup", "#{module_name}-#{Process.pid}")
135
+ FileUtils.mkdir_p(File.dirname(backup))
136
+ FileUtils.mv(destination, backup)
137
+ end
138
+ FileUtils.mkdir_p(File.dirname(destination))
139
+ FileUtils.mv(staging, destination)
140
+ FileUtils.rm_rf(backup) if backup
141
+ backup = nil
142
+ digest = tree_digest(destination)
143
+ write_lock(canonical_entry, module_name, digest, dependencies: declared_dependencies(manifest))
144
+ Installation.new(entry, module_name, destination, digest)
145
+ rescue StandardError
146
+ FileUtils.mv(backup, destination) if backup && File.exist?(backup) && !File.exist?(destination)
147
+ raise
148
+ ensure
149
+ FileUtils.rm_rf(staging) if staging && File.exist?(staging)
150
+ FileUtils.rm_rf(backup) if backup && File.exist?(backup)
151
+ end
152
+ rescue KeyError, JSON::ParserError => e
153
+ raise MarketplaceError, "invalid module manifest: #{e.message}"
154
+ end
155
+
156
+ def resolve(identifier, version: nil)
157
+ path = File.expand_path(identifier.to_s)
158
+ return [local_entry(path), path] if File.directory?(path)
159
+
160
+ entry = version ? @catalog.find_version(identifier, version) : @catalog.find(identifier)
161
+ [entry, entry.source]
162
+ end
163
+
164
+ def resolve_installed_module_name(identifier)
165
+ lock_path = File.join(@target, ".mxrb", "modules.lock.json")
166
+ raise MarketplaceError, "no modules are installed (lock file missing)" unless File.file?(lock_path)
167
+
168
+ lock = JSON.parse(File.read(lock_path))
169
+ locked = lock.fetch("modules", {})
170
+ entry = locked.find { |_name, info| info["package"] == identifier.to_s }
171
+ unless entry
172
+ candidates = locked.select { |name, _| name.downcase == identifier.to_s.downcase }
173
+ entry = candidates.first
174
+ end
175
+ raise MarketplaceError, "module #{identifier.inspect} is not installed" unless entry
176
+
177
+ entry.first
178
+ end
179
+
180
+ def remove_from_lock(module_name)
181
+ lock_path = File.join(@target, ".mxrb", "modules.lock.json")
182
+ return unless File.file?(lock_path)
183
+
184
+ lock = JSON.parse(File.read(lock_path))
185
+ lock.fetch("modules", {}).delete(module_name)
186
+ temporary = "#{lock_path}.tmp-#{Process.pid}"
187
+ File.write(temporary, JSON.pretty_generate(lock) << "\n")
188
+ File.rename(temporary, lock_path)
189
+ ensure
190
+ FileUtils.rm_f(temporary) if defined?(temporary) && temporary && File.exist?(temporary)
191
+ end
192
+
193
+ def local_entry(path)
194
+ Entry.new(File.basename(path), "local", "Local MXRB module", path, nil)
195
+ end
196
+
197
+ def materialize(source, entry, workspace)
198
+ return builtin_path(source) if source.start_with?("builtin:")
199
+ return File.expand_path(source) if File.directory?(source)
200
+
201
+ destination = File.join(workspace, "repository")
202
+ command = ["git", "clone", "--depth", "1"]
203
+ command += ["--branch", entry.ref] if entry.ref
204
+ command += [source, destination]
205
+ output, status = Open3.capture2e(*command)
206
+ raise MarketplaceError, "could not fetch #{entry.name}: #{output}" unless status.success?
207
+
208
+ destination
209
+ end
210
+
211
+ def builtin_path(source)
212
+ slug = source.delete_prefix("builtin:")
213
+ unless slug.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
214
+ raise MarketplaceError, "invalid built-in module slug #{slug.inspect}"
215
+ end
216
+ path = File.expand_path("../../marketplace/modules/#{slug}", __dir__)
217
+ raise MarketplaceError, "built-in module #{slug.inspect} is unavailable" unless File.directory?(path)
218
+
219
+ path
220
+ end
221
+
222
+ def load_manifest(package)
223
+ JSON.parse(File.read(File.join(package, MANIFEST)))
224
+ rescue Errno::ENOENT => e
225
+ raise MarketplaceError, "module manifest is missing: #{e.message}"
226
+ end
227
+
228
+ def valid_name(value)
229
+ name = value.to_s
230
+ unless name.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/)
231
+ raise MarketplaceError, "invalid module name #{name.inspect}"
232
+ end
233
+
234
+ name
235
+ end
236
+
237
+ def package_files(package, manifest)
238
+ files = Array(manifest["files"] || Dir.children(package) - [MANIFEST])
239
+ raise MarketplaceError, "module package has no files" if files.empty?
240
+
241
+ files.each do |relative|
242
+ path = Pathname.new(relative.to_s)
243
+ if path.absolute? || path.each_filename.any? { _1 == ".." }
244
+ raise MarketplaceError, "unsafe module path #{relative.inspect}"
245
+ end
246
+ raise MarketplaceError, "module file is missing: #{relative}" unless File.exist?(File.join(package, relative))
247
+ end
248
+ files
249
+ end
250
+
251
+ def copy_entry(package, staging, relative)
252
+ source = File.join(package, relative)
253
+ destination = File.join(staging, relative)
254
+ FileUtils.mkdir_p(File.dirname(destination))
255
+ FileUtils.cp_r(source, destination)
256
+ end
257
+
258
+ def tree_digest(path)
259
+ digest = Digest::SHA256.new
260
+ Dir.glob(File.join(path, "**", "*"), File::FNM_DOTMATCH).sort.each do |file|
261
+ next unless File.file?(file)
262
+
263
+ digest << file.delete_prefix("#{path}/") << "\0" << File.binread(file)
264
+ end
265
+ digest.hexdigest
266
+ end
267
+
268
+ def declared_dependencies(manifest)
269
+ Array(manifest["dependencies"]).map do |dep|
270
+ name = dep.is_a?(Hash) ? dep["name"].to_s : dep.to_s
271
+ name unless name.empty?
272
+ end.compact
273
+ end
274
+
275
+ def validate_dependencies!(manifest)
276
+ deps = declared_dependencies(manifest)
277
+ return if deps.empty?
278
+
279
+ lock_path = File.join(@target, ".mxrb", "modules.lock.json")
280
+ installed_packages = if File.file?(lock_path)
281
+ JSON.parse(File.read(lock_path)).fetch("modules", {}).values.map { _1["package"] }
282
+ else
283
+ []
284
+ end
285
+ missing = deps.reject { |dep| installed_packages.include?(dep) }
286
+ return if missing.empty?
287
+
288
+ raise MarketplaceError,
289
+ "missing dependencies: #{missing.join(', ')} — install them first"
290
+ end
291
+
292
+ def validate_mendix_version!(manifest)
293
+ required = manifest["mendix_version"].to_s.strip
294
+ return if required.empty?
295
+
296
+ project_version = detect_project_mendix_version
297
+ return if project_version.nil?
298
+
299
+ unless mendix_version_compatible?(project_version, required)
300
+ raise MarketplaceError,
301
+ "module requires Mendix #{required}, but the project uses #{project_version}"
302
+ end
303
+ end
304
+
305
+ def detect_project_mendix_version
306
+ mpr_files = Dir.glob(File.join(@target, "*.mpr"))
307
+ return nil if mpr_files.empty?
308
+
309
+ require_relative "../mxrb/io/mpr_file"
310
+ mpr = IO::MprFile.open(mpr_files.first, readonly: true)
311
+ mpr.mendix_version
312
+ rescue StandardError
313
+ nil
314
+ ensure
315
+ mpr&.close
316
+ end
317
+
318
+ def mendix_version_compatible?(project_version, required)
319
+ proj_parts = project_version.to_s.split(".").map(&:to_i)
320
+ req_str = required.to_s.strip
321
+
322
+ case req_str
323
+ when /\A(\d+)\.x\z/
324
+ proj_parts[0] == ::Regexp.last_match(1).to_i
325
+ when /\A>=\s*(.+)\z/
326
+ version_gte?(proj_parts, ::Regexp.last_match(1).strip.split(".").map(&:to_i))
327
+ when /\A~>\s*(.+)\z/
328
+ base = ::Regexp.last_match(1).strip.split(".").map(&:to_i)
329
+ version_gte?(proj_parts, base) &&
330
+ proj_parts[0, base.size - 1] == base[0, base.size - 1]
331
+ else
332
+ project_version.start_with?(req_str.sub(/\.\*\z/, ""))
333
+ end
334
+ end
335
+
336
+ def version_gte?(a, b)
337
+ a.zip(b).each do |av, bv|
338
+ av ||= 0
339
+ bv ||= 0
340
+ return true if av > bv
341
+ return false if av < bv
342
+ end
343
+ true
344
+ end
345
+
346
+ def validate_no_dependents!(module_name)
347
+ lock_path = File.join(@target, ".mxrb", "modules.lock.json")
348
+ return unless File.file?(lock_path)
349
+
350
+ lock = JSON.parse(File.read(lock_path))
351
+ package_name = lock.dig("modules", module_name, "package")
352
+ return unless package_name
353
+
354
+ dependents = lock.fetch("modules", {}).filter_map do |name, info|
355
+ name if name != module_name && Array(info["dependencies"]).include?(package_name)
356
+ end
357
+ return if dependents.empty?
358
+
359
+ raise MarketplaceError,
360
+ "cannot remove #{module_name.inspect}: #{dependents.join(', ')} depend on it"
361
+ end
362
+
363
+ def write_lock(entry, module_name, digest, dependencies: [])
364
+ lock_path = File.join(@target, ".mxrb", "modules.lock.json")
365
+ FileUtils.mkdir_p(File.dirname(lock_path))
366
+ lock = File.file?(lock_path) ? JSON.parse(File.read(lock_path)) : { "modules" => {} }
367
+ lock["modules"][module_name] = {
368
+ "package" => entry.name, "version" => entry.version,
369
+ "source" => entry.source, "ref" => entry.ref, "sha256" => digest,
370
+ "dependencies" => dependencies.empty? ? nil : dependencies
371
+ }.compact
372
+ temporary = "#{lock_path}.tmp-#{Process.pid}"
373
+ File.write(temporary, JSON.pretty_generate(lock) << "\n")
374
+ File.rename(temporary, lock_path)
375
+ ensure
376
+ FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Mxrb
6
+ module Model
7
+ # Associations are embedded in DomainModel BSON — NOT separate Unit rows.
8
+ # $Type: DomainModels$Association
9
+ #
10
+ # CRITICAL GOTCHA (from reverse engineering):
11
+ # ParentID = entity FROM (the one with the FK) — counter-intuitive naming!
12
+ # ChildID = entity TO (the referenced entity)
13
+ # MemberAccess should be added ONLY to the FROM entity (ParentPointer), not ChildID.
14
+ class Association
15
+ attr_accessor :id, :name, :documentation,
16
+ :from_entity_id, # ParentID in BSON (FROM entity — has the FK)
17
+ :to_entity_id, # ChildID in BSON (TO entity — referenced)
18
+ :association_type, :owner, :storage_format,
19
+ :delete_behavior, :export_level
20
+
21
+ TYPES = %i[Reference ReferenceSet].freeze
22
+ OWNERS = %i[Default Both].freeze
23
+ STORAGE_FMTS = %i[Column Table].freeze
24
+
25
+ def self.from_bson(doc, _domain_model_id = nil, _mpr = nil)
26
+ a = new
27
+ a.id = IO::BsonCodec.extract_id(doc["$ID"] || doc["\$ID"])
28
+ a.name = doc["Name"] || doc["name"]
29
+ a.documentation = doc["Documentation"] || doc["documentation"] || ""
30
+ # ParentID = FROM entity (counter-intuitive — see class comment)
31
+ a.from_entity_id = IO::BsonCodec.extract_id(
32
+ doc["ParentPointer"] || doc["ParentID"] || doc["parentId"]
33
+ )
34
+ child = doc['Child'] || doc["ChildPointer"] || doc["ChildID"] || doc["childId"]
35
+ a.to_entity_id = if child.is_a?(String) && child.include?('.')
36
+ child
37
+ else
38
+ IO::BsonCodec.extract_id(child)
39
+ end
40
+ a.association_type = (doc["Type"] || doc["type"] || "Reference").to_sym
41
+ a.owner = (doc["Owner"] || doc["owner"] || "Default").to_sym
42
+ a.storage_format = (doc["StorageFormat"] || "Column").to_sym
43
+ a.delete_behavior = doc["DeleteBehavior"] || doc["deleteBehavior"]
44
+ a.export_level = doc["ExportLevel"] || "Hidden"
45
+ a
46
+ end
47
+
48
+ def to_bson
49
+ {
50
+ "$ID" => @id || SecureRandom.uuid,
51
+ "$Type" => "DomainModels$Association",
52
+ "Name" => @name,
53
+ "Documentation" => @documentation || "",
54
+ # ParentID = FROM entity (the one that carries the FK)
55
+ "ParentID" => @from_entity_id,
56
+ "ChildID" => @to_entity_id,
57
+ "Type" => (@association_type || :Reference).to_s,
58
+ "Owner" => (@owner || :Default).to_s,
59
+ "StorageFormat" => (@storage_format || :Column).to_s,
60
+ "DeleteBehavior" => @delete_behavior || default_delete_behavior,
61
+ "ExportLevel" => @export_level || "Hidden",
62
+ }
63
+ end
64
+
65
+ def inspect
66
+ "#<Mxrb::Association name=#{@name.inspect} type=#{@association_type} " \
67
+ "from=#{@from_entity_id.to_s.slice(0, 8)} to=#{@to_entity_id.to_s.slice(0, 8)}>"
68
+ end
69
+
70
+ def parent_delete_behavior
71
+ behavior_value('parentDeleteBehavior', 'ParentDeleteBehavior')
72
+ end
73
+
74
+ def child_delete_behavior
75
+ behavior_value('childDeleteBehavior', 'ChildDeleteBehavior')
76
+ end
77
+
78
+ private
79
+
80
+ def behavior_value(lower, upper)
81
+ return :NoAction unless @delete_behavior.is_a?(Hash)
82
+
83
+ (@delete_behavior[lower] || @delete_behavior[upper] || 'NoAction').to_sym
84
+ end
85
+
86
+ def default_delete_behavior
87
+ {
88
+ "$ID" => SecureRandom.uuid,
89
+ "$Type" => "DomainModels$DeleteBehavior",
90
+ "parentDeleteBehavior" => "NoAction",
91
+ "childDeleteBehavior" => "NoAction",
92
+ "parentErrorMessage" => nil,
93
+ "childErrorMessage" => nil,
94
+ }
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Mxrb
6
+ module Model
7
+ # Attributes are embedded in Entity BSON — NOT separate Unit rows.
8
+ # $Type: DomainModels$Attribute
9
+ class Attribute
10
+ # Maps DSL symbol → Mendix storage type name
11
+ TYPE_MAP = {
12
+ string: "DomainModels$StringAttributeType",
13
+ integer: "DomainModels$IntegerAttributeType",
14
+ long: "DomainModels$LongAttributeType",
15
+ float: "DomainModels$FloatAttributeType",
16
+ decimal: "DomainModels$DecimalAttributeType",
17
+ boolean: "DomainModels$BooleanAttributeType",
18
+ datetime: "DomainModels$DateTimeAttributeType",
19
+ autonumber: "DomainModels$AutoNumberAttributeType",
20
+ hashstring: "DomainModels$HashedStringAttributeType",
21
+ binary: "DomainModels$BinaryAttributeType",
22
+ enum: "DomainModels$EnumerationAttributeType",
23
+ }.freeze
24
+
25
+ REVERSE_TYPE_MAP = TYPE_MAP.invert.freeze
26
+
27
+ attr_accessor :id, :name, :documentation, :type, :default_value,
28
+ :data_storage_guid, :export_level, :raw_type_doc, :raw_value_doc,
29
+ :length, :localize_date, :enumeration, :required, :unique
30
+
31
+ def self.from_bson(doc)
32
+ a = new
33
+ a.id = IO::BsonCodec.extract_id(doc["$ID"] || doc["\$ID"])
34
+ a.name = doc["name"] || doc["Name"]
35
+ a.documentation = doc["documentation"] || doc["Documentation"] || ""
36
+ a.data_storage_guid = doc["dataStorageGuid"] || doc["DataStorageGuid"]
37
+ a.export_level = doc["exportLevel"] || doc["ExportLevel"] || "Hidden"
38
+
39
+ type_doc = doc["type"] || doc["Type"] || doc["newType"] || doc["NewType"]
40
+ a.raw_type_doc = type_doc
41
+ if type_doc.is_a?(Hash)
42
+ storage_type = type_doc["$Type"] || type_doc["\$Type"]
43
+ a.type = REVERSE_TYPE_MAP[storage_type] || :string
44
+ a.length = type_doc['length'] || type_doc['Length']
45
+ a.localize_date = if type_doc.key?('localizeDate')
46
+ type_doc['localizeDate']
47
+ else
48
+ type_doc['LocalizeDate']
49
+ end
50
+ a.enumeration = type_doc['enumeration'] || type_doc['Enumeration']
51
+ else
52
+ a.type = :string
53
+ end
54
+
55
+ a.raw_value_doc = doc["value"] || doc["Value"]
56
+ a.default_value = extract_default(a.raw_value_doc)
57
+ a
58
+ end
59
+
60
+ def to_bson
61
+ {
62
+ "$ID" => @id || SecureRandom.uuid,
63
+ "$Type" => "DomainModels$Attribute",
64
+ "name" => @name,
65
+ "documentation" => @documentation || "",
66
+ "dataStorageGuid" => @data_storage_guid || SecureRandom.uuid,
67
+ "exportLevel" => @export_level || "Hidden",
68
+ "type" => serialize_type,
69
+ "value" => serialize_value,
70
+ }
71
+ end
72
+
73
+ def inspect
74
+ "#<Mxrb::Attribute name=#{@name.inspect} type=#{@type}>"
75
+ end
76
+
77
+ private
78
+
79
+ def self.extract_default(value_doc)
80
+ return nil unless value_doc.is_a?(Hash)
81
+ value_doc["defaultValue"] || value_doc["DefaultValue"]
82
+ end
83
+
84
+ def serialize_type
85
+ storage_type = TYPE_MAP[@type] || TYPE_MAP[:string]
86
+ base = { "$ID" => SecureRandom.uuid, "$Type" => storage_type }
87
+ base["localizeDate"] = true if @type == :datetime
88
+ base
89
+ end
90
+
91
+ def serialize_value
92
+ {
93
+ "$ID" => SecureRandom.uuid,
94
+ "$Type" => "DomainModels$StoredValue",
95
+ "defaultValue" => @default_value.to_s,
96
+ }
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module Model
5
+ # A marketplace connector detected inside a project.
6
+ #
7
+ # MXRB never runs the connector; it only represents the public surface that
8
+ # the semantic index can observe. Protected or non-decodable internals are
9
+ # reported as unavailable (empty), never guessed.
10
+ Connector = Data.define(
11
+ :module_name, :protocol, :appstore_guid, :appstore_version,
12
+ :entities, :microflows, :protected, :metadata
13
+ ) do
14
+ def known? = !protocol.nil?
15
+ end
16
+ end
17
+ end