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,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module Mxrb
7
+ module Model
8
+ # Materializes the typed design-system contract into MXRB-owned theme files.
9
+ class DesignMaterializer
10
+ IMPORT = '@import "mxrb-design-system";'
11
+
12
+ def initialize(root, definition)
13
+ @root = File.expand_path(root)
14
+ @definition = definition
15
+ end
16
+
17
+ def materialize!
18
+ tokens = valued_tokens(@definition.fetch(:tokens, []))
19
+ themes = resolved_themes
20
+ return self if tokens.empty? && themes.empty?
21
+
22
+ write(File.join('theme', 'web', '_mxrb-design-system.scss'), stylesheet(tokens, themes))
23
+ ensure_import
24
+ write(
25
+ File.join('themesource', 'mxrb', 'web', 'design-properties.json'),
26
+ JSON.pretty_generate(design_properties(tokens)) << "\n"
27
+ )
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ def valued_tokens(tokens) = Array(tokens).reject { _1[:value].nil? }
34
+
35
+ def resolved_themes
36
+ themes = Array(@definition[:themes]).to_h { [_1.fetch(:name).to_s, _1] }
37
+ themes.keys.sort.map { [_1, resolve_theme(_1, themes, [])] }
38
+ end
39
+
40
+ def resolve_theme(name, themes, trail)
41
+ return [] unless themes.key?(name)
42
+ raise SerializationError, "design-system theme inheritance cycle: #{(trail + [name]).join(' -> ')}" \
43
+ if trail.include?(name)
44
+
45
+ theme = themes.fetch(name)
46
+ inherited = theme[:inherits] ? resolve_theme(theme[:inherits].to_s, themes, trail + [name]) : []
47
+ merge_tokens(inherited, valued_tokens(theme.fetch(:tokens, [])))
48
+ end
49
+
50
+ def merge_tokens(inherited, own) = (inherited + own).reverse.uniq { token_property(_1) }.reverse
51
+
52
+ def stylesheet(tokens, themes)
53
+ sections = []
54
+ sections << css_block(':root', tokens) unless tokens.empty?
55
+ themes.reject { _2.empty? }.each do |name, theme_tokens|
56
+ slug = slug(name)
57
+ sections << css_block(
58
+ %(:root[data-mxrb-theme="#{slug}"], .mxrb-theme-#{slug}),
59
+ theme_tokens
60
+ )
61
+ end
62
+ "/* Generated by MXRB from the Ruby design_system contract. */\n#{sections.join("\n\n")}\n"
63
+ end
64
+
65
+ def css_block(selector, tokens)
66
+ declarations = tokens.map do |token|
67
+ " #{token_property(token)}: #{safe_value(token.fetch(:value))};"
68
+ end
69
+ "#{selector} {\n#{declarations.join("\n")}\n}"
70
+ end
71
+
72
+ def token_property(token)
73
+ name = token.fetch(:name).to_s
74
+ return name if name.match?(/\A--[a-zA-Z0-9_-]+\z/)
75
+ return "--#{slug(name.delete_prefix('$'))}" if name.start_with?('$')
76
+
77
+ "--#{slug(token.fetch(:kind))}-#{slug(name)}"
78
+ end
79
+
80
+ def safe_value(value)
81
+ text = value.to_s.strip
82
+ raise SerializationError, "unsafe design-system token value: #{value.inspect}" \
83
+ if text.empty? || text.match?(/[\n\r;{}]/)
84
+
85
+ text
86
+ end
87
+
88
+ def slug(value)
89
+ result = value.to_s.gsub(/([a-z\d])([A-Z])/, '\1-\2')
90
+ .downcase.gsub(/[^a-z0-9_-]+/, '-').gsub(/\A-+|-+\z/, '')
91
+ raise SerializationError, "invalid design-system name: #{value.inspect}" if result.empty?
92
+
93
+ result
94
+ end
95
+
96
+ def design_properties(tokens)
97
+ colors = tokens.select { _1.fetch(:kind).to_sym == :color }.map do |token|
98
+ {
99
+ 'category' => 'MXRB design system',
100
+ 'name' => human_name(token.fetch(:name)),
101
+ 'type' => 'ColorPicker',
102
+ 'property' => token_property(token)
103
+ }
104
+ end
105
+ { 'Widget' => colors }
106
+ end
107
+
108
+ def human_name(name) = slug(name.to_s.delete_prefix('$').delete_prefix('--'))
109
+ .split('-').map(&:capitalize).join(' ')
110
+
111
+ def ensure_import
112
+ path = File.join(@root, 'theme', 'web', 'main.scss')
113
+ current = File.file?(path) ? File.binread(path) : ''
114
+ return if current.lines.map(&:strip).include?(IMPORT)
115
+
116
+ separator = current.empty? || current.end_with?("\n") ? '' : "\n"
117
+ write(File.join('theme', 'web', 'main.scss'), "#{current}#{separator}#{IMPORT}\n")
118
+ end
119
+
120
+ def write(relative, contents)
121
+ target = File.join(@root, relative)
122
+ temporary = "#{target}.mxrb-#{Process.pid}"
123
+ begin
124
+ FileUtils.mkdir_p(File.dirname(target))
125
+ File.binwrite(temporary, contents)
126
+ FileUtils.mv(temporary, target)
127
+ ensure
128
+ FileUtils.rm_f(temporary)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+
6
+ module Mxrb
7
+ module Model
8
+ DesignMigrationChange = Data.define(:path, :before, :after, :occurrences)
9
+
10
+ # Immutable preview of literal-to-token stylesheet replacements.
11
+ class DesignMigrationPlan
12
+ attr_reader :changes
13
+
14
+ def self.build(root, replacements)
15
+ pairs = replacements.to_h.transform_keys(&:to_s).transform_values(&:to_s)
16
+ changes = stylesheet_paths(root).filter_map { migration_change(root, _1, pairs) }
17
+ new(root, changes)
18
+ end
19
+
20
+ def self.stylesheet_paths(root)
21
+ paths = DesignSystem::ASSET_DIRECTORIES.flat_map do |directory|
22
+ Dir.glob(File.join(root, directory, '**', '*.{css,scss}'))
23
+ end
24
+ paths.select { File.file?(_1) && !File.symlink?(_1) }.sort
25
+ end
26
+
27
+ def self.migration_change(root, path, pairs)
28
+ before = File.binread(path)
29
+ after = pairs.reduce(before) { |text, (literal, token)| text.gsub(literal, token) }
30
+ return if after == before
31
+
32
+ relative = Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
33
+ occurrences = pairs.sum { |literal, _| before.scan(literal).size }
34
+ DesignMigrationChange.new(relative, before, after, occurrences)
35
+ end
36
+ private_class_method :stylesheet_paths, :migration_change
37
+
38
+ def initialize(root, changes)
39
+ @root = File.expand_path(root)
40
+ @changes = changes.freeze
41
+ @digests = changes.to_h { [_1.path, Digest::SHA256.hexdigest(_1.before)] }.freeze
42
+ @after_digests = changes.to_h { [_1.path, Digest::SHA256.hexdigest(_1.after)] }.freeze
43
+ @applied = false
44
+ end
45
+
46
+ def empty? = @changes.empty?
47
+ def applied? = @applied
48
+
49
+ def apply!
50
+ raise ArgumentError, 'design migration plan was already applied' if @applied
51
+
52
+ verify_changes!(@digests, 'changed after preview')
53
+ replace_all!(fallback: :before, &:after)
54
+ @applied = true
55
+ self
56
+ end
57
+
58
+ # Restores the previewed contents when a surrounding batch fails.
59
+ def rollback!
60
+ return self unless @applied
61
+
62
+ verify_changes!(@after_digests, 'changed after migration')
63
+ replace_all!(fallback: :after, &:before)
64
+ @applied = false
65
+ self
66
+ end
67
+
68
+ private
69
+
70
+ def verify_changes!(digests, reason)
71
+ @changes.each do |change|
72
+ target = File.join(@root, change.path)
73
+ unless File.file?(target) && Digest::SHA256.file(target).hexdigest == digests.fetch(change.path)
74
+ raise SerializationError, "design asset #{reason}: #{change.path}"
75
+ end
76
+ end
77
+ end
78
+
79
+ def replace_all!(fallback:)
80
+ staged = {}
81
+ staged = stage_changes { yield(_1) }
82
+ commit_staged!(staged, fallback)
83
+ ensure
84
+ staged.each_value { FileUtils.rm_f(_1) }
85
+ end
86
+
87
+ def stage_changes
88
+ @changes.to_h do |change|
89
+ temporary = temporary_path(change.path)
90
+ File.binwrite(temporary, yield(change))
91
+ [change.path, temporary]
92
+ end
93
+ end
94
+
95
+ def commit_staged!(staged, fallback)
96
+ committed = []
97
+ @changes.each do |change|
98
+ FileUtils.mv(staged.fetch(change.path), File.join(@root, change.path))
99
+ committed << change
100
+ end
101
+ rescue StandardError
102
+ restore_committed!(committed, fallback)
103
+ raise
104
+ end
105
+
106
+ def restore_committed!(committed, fallback)
107
+ committed.reverse_each do |change|
108
+ target = File.join(@root, change.path)
109
+ temporary = "#{target}.mxrb-rollback-#{Process.pid}-#{staged_suffix(change.path)}"
110
+ File.binwrite(temporary, change.public_send(fallback))
111
+ FileUtils.mv(temporary, target)
112
+ ensure
113
+ FileUtils.rm_f(temporary)
114
+ end
115
+ end
116
+
117
+ def staged_suffix(path)
118
+ Digest::SHA256.hexdigest(path)[0, 12]
119
+ end
120
+
121
+ def temporary_path(path)
122
+ "#{File.join(@root, path)}.mxrb-#{Process.pid}-#{staged_suffix(path)}"
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Mxrb
6
+ module Model
7
+ DesignToken = Data.define(:name, :value, :kind, :theme, :path, :line)
8
+
9
+ # Read-only inventory and quality metrics for Mendix theme assets.
10
+ class DesignSystem
11
+ ASSET_DIRECTORIES = %w[
12
+ theme theme-cache themesource resources widgets javasource javascriptsource
13
+ userlib vendorlib
14
+ ].freeze
15
+ CSS_TOKEN = /(?<name>--[A-Za-z0-9_-]+)\s*:\s*(?<value>[^;{}]+);/
16
+ SCSS_TOKEN = /(?<name>\$[A-Za-z0-9_-]+)\s*:\s*(?<value>[^;{}]+);/
17
+
18
+ attr_reader :root
19
+
20
+ def initialize(root)
21
+ @root = File.expand_path(root)
22
+ end
23
+
24
+ def tokens
25
+ @tokens ||= stylesheet_paths.flat_map { scan_stylesheet(_1) }.freeze
26
+ end
27
+
28
+ def themes
29
+ tokens.map(&:theme).compact.uniq.sort.freeze
30
+ end
31
+
32
+ def catalogs
33
+ Dir.glob(File.join(@root, 'themesource', '**', 'design-properties.json')).sort.to_h do |path|
34
+ [relative(path), JSON.parse(File.read(path))]
35
+ rescue JSON::ParserError
36
+ [relative(path), nil]
37
+ end.freeze
38
+ end
39
+
40
+ def unresolved_references
41
+ names = tokens.map(&:name).to_h { [_1, true] }
42
+ tokens.flat_map do |token|
43
+ token.value.scan(/var\((--[A-Za-z0-9_-]+)/).flatten.reject { names.key?(_1) }
44
+ .map do
45
+ {
46
+ token:, reference: _1
47
+ }.freeze
48
+ end
49
+ end.freeze
50
+ end
51
+
52
+ def literal_colors
53
+ tokens.select { _1.value.match?(/#[0-9a-f]{3,8}\b/i) }.freeze
54
+ end
55
+
56
+ def contrast_ratio(foreground, background)
57
+ light, dark = [relative_luminance(foreground), relative_luminance(background)].sort.reverse
58
+ ((light + 0.05) / (dark + 0.05)).round(2)
59
+ end
60
+
61
+ def plan_literal_migration(replacements)
62
+ DesignMigrationPlan.build(@root, replacements)
63
+ end
64
+
65
+ private
66
+
67
+ def stylesheet_paths
68
+ paths = ASSET_DIRECTORIES.flat_map do |directory|
69
+ Dir.glob(File.join(@root, directory, '**', '*.{css,scss}'))
70
+ end
71
+ paths.select { File.file?(_1) }.sort
72
+ end
73
+
74
+ def scan_stylesheet(path)
75
+ theme = File.basename(path)[/\A_theme-([^.]+)/, 1]
76
+ File.readlines(path).each_with_index.flat_map do |line, index|
77
+ next [] if line.lstrip.start_with?('//')
78
+
79
+ line.scan(CSS_TOKEN).map { token(_1, :css_custom_property, theme, path, index) } +
80
+ line.scan(SCSS_TOKEN).map { token(_1, :scss_variable, theme, path, index) }
81
+ end
82
+ end
83
+
84
+ def token(match, kind, theme, path, index)
85
+ DesignToken.new(match[0], match[1].strip, kind, theme, relative(path), index + 1)
86
+ end
87
+
88
+ def relative(path) = Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s
89
+
90
+ def relative_luminance(color)
91
+ hex = normalized_hex(color)
92
+ channels = hex.scan(/../).map { linear_channel(_1.to_i(16) / 255.0) }
93
+ (0.2126 * channels[0]) + (0.7152 * channels[1]) + (0.0722 * channels[2])
94
+ end
95
+
96
+ def normalized_hex(color)
97
+ hex = color.to_s.delete_prefix('#')
98
+ hex = hex.chars.flat_map { [_1, _1] }.join if hex.length == 3
99
+ raise ArgumentError, 'expected a three- or six-digit hex color' unless hex.match?(/\A[0-9a-f]{6}\z/i)
100
+
101
+ hex
102
+ end
103
+
104
+ def linear_channel(channel)
105
+ return channel / 12.92 if channel <= 0.04045
106
+
107
+ ((channel + 0.055) / 1.055)**2.4
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "unit"
4
+ require_relative "entity"
5
+ require_relative "association"
6
+
7
+ module Mxrb
8
+ module Model
9
+ # $Type: DomainModels$DomainModel
10
+ # ContainmentName: "DomainModel"
11
+ # Unlike entities/attributes, DomainModel IS its own Unit row.
12
+ # Entities and Attributes are embedded inside the DomainModel BSON (not separate Unit rows).
13
+ class DomainModel < Unit
14
+ attr_reader :documentation, :annotations
15
+
16
+ def decode(doc)
17
+ @documentation = doc["documentation"] || doc["Documentation"] || ""
18
+ @annotations = parse_array(doc["annotations"] || doc["Annotations"])
19
+
20
+ entity_docs = parse_array(doc["entities"] || doc["Entities"])
21
+ @entities = entity_docs.map { Entity.from_bson(_1, @id, @mpr) }
22
+
23
+ assoc_docs = parse_array(doc["associations"] || doc["Associations"])
24
+ @associations = assoc_docs.map { Association.from_bson(_1, @id, @mpr) }
25
+ cross_docs = parse_array(doc["crossAssociations"] || doc["CrossAssociations"])
26
+ @cross_associations = cross_docs.map { Association.from_bson(_1, @id, @mpr) }
27
+ end
28
+
29
+ def entities = @entities || []
30
+ def local_associations = @associations || []
31
+ def cross_associations = @cross_associations || []
32
+ def associations = local_associations + cross_associations
33
+
34
+ def to_bson
35
+ {
36
+ "$ID" => @id,
37
+ "$Type" => "DomainModels$DomainModel",
38
+ "entities" => IO::BsonCodec.build_array(@entities.map(&:to_bson)),
39
+ "associations" => IO::BsonCodec.build_array(local_associations.map(&:to_bson)),
40
+ "crossAssociations" => IO::BsonCodec.build_array(cross_associations.map(&:to_bson)),
41
+ "annotations" => IO::BsonCodec.build_array([]),
42
+ "documentation" => @documentation || "",
43
+ }
44
+ end
45
+
46
+ def inspect
47
+ "#<Mxrb::DomainModel entities=#{entities.size} associations=#{associations.size}>"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute"
4
+
5
+ module Mxrb
6
+ module Model
7
+ # Entities are embedded in the DomainModel BSON — NOT separate Unit rows.
8
+ # $Type: DomainModels$Entity (read) / DomainModels$EntityImpl (write)
9
+ class Entity # rubocop:disable Metrics/ClassLength
10
+ attr_accessor :id, :name, :qualified_name, :documentation,
11
+ :persistable, :location, :data_storage_guid,
12
+ :export_level, :generalization, :access_rules, :indexes,
13
+ :system_members
14
+
15
+ # Build from a BSON hash (embedded in DomainModel's "entities" array).
16
+ def self.from_bson(doc, _domain_model_id, mpr)
17
+ e = new
18
+ e.id = IO::BsonCodec.extract_id(doc["$ID"] || doc["\$ID"])
19
+ e.name = doc["name"] || doc["Name"]
20
+ e.qualified_name = doc["$QualifiedName"] || doc["\$QualifiedName"]
21
+ e.documentation = doc["documentation"] || doc["Documentation"] || ""
22
+ e.data_storage_guid = doc["dataStorageGuid"] || doc["DataStorageGuid"]
23
+ e.export_level = doc["exportLevel"] || doc["ExportLevel"] || "Hidden"
24
+ e.location = parse_location(doc["location"] || doc["Location"])
25
+
26
+ # Persistable lives inside generalization.NoGeneralization.persistable
27
+ gen = doc["generalization"] || doc["Generalization"] ||
28
+ doc["maybeGeneralization"] || doc["MaybeGeneralization"]
29
+ e.generalization = gen
30
+ persistable = if gen.is_a?(Hash)
31
+ gen.fetch('persistable', gen.fetch('Persistable', true))
32
+ else
33
+ true
34
+ end
35
+ e.persistable = persistable != false
36
+ e.system_members = parse_system_members(gen)
37
+
38
+ # Attributes (embedded array with marker)
39
+ attr_arr = IO::BsonCodec.parse_array(doc["attributes"] || doc["Attributes"])[:items]
40
+ e.instance_variable_set(:@attributes, attr_arr.map { Attribute.from_bson(_1) })
41
+ rules = IO::BsonCodec.parse_array(doc['validationRules'] || doc['ValidationRules'])[:items]
42
+ apply_validation_rules(e.attributes, rules)
43
+ raw_indexes = IO::BsonCodec.parse_array(doc['indexes'] || doc['Indexes'])[:items]
44
+ e.indexes = normalize_indexes(raw_indexes, e.attributes, e.qualified_name)
45
+
46
+ # Access rules (embedded array with marker 3)
47
+ rule_arr = IO::BsonCodec.parse_array(doc["accessRules"] || doc["AccessRules"])[:items]
48
+ e.access_rules = rule_arr.map { parse_access_rule(_1) }
49
+
50
+ e
51
+ end
52
+
53
+ def attributes = @attributes || []
54
+
55
+ def generalization_target
56
+ return unless @generalization.is_a?(Hash)
57
+
58
+ @generalization['generalization'] || @generalization['Generalization']
59
+ end
60
+
61
+ def to_bson
62
+ {
63
+ "$ID" => @id,
64
+ "$Type" => "DomainModels$EntityImpl",
65
+ "$QualifiedName" => @qualified_name,
66
+ "name" => @name,
67
+ "documentation" => @documentation || "",
68
+ "dataStorageGuid" => @data_storage_guid || SecureRandom.uuid,
69
+ "location" => serialize_location(@location),
70
+ "generalization" => serialize_generalization,
71
+ "attributes" => IO::BsonCodec.build_array(@attributes.map(&:to_bson)),
72
+ "validationRules" => IO::BsonCodec.build_array([]), # must come after attributes
73
+ "eventHandlers" => IO::BsonCodec.build_array([]),
74
+ "indexes" => IO::BsonCodec.build_array([]),
75
+ "accessRules" => IO::BsonCodec.build_array(@access_rules.to_a),
76
+ "source" => nil,
77
+ "exportLevel" => @export_level || "Hidden",
78
+ "image" => "",
79
+ "imageData" => "",
80
+ }
81
+ end
82
+
83
+ def inspect
84
+ "#<Mxrb::Entity name=#{@name.inspect} attrs=#{attributes.size} persistable=#{@persistable}>"
85
+ end
86
+
87
+ private
88
+
89
+ def self.apply_validation_rules(attributes, rules)
90
+ by_name = attributes.to_h { [_1.name, _1] }
91
+ rules.each do |rule|
92
+ attribute = by_name[rule['Attribute'].to_s.split('.').last]
93
+ next unless attribute
94
+
95
+ kind = rule.dig('RuleInfo', '$Type').to_s
96
+ attribute.required = true if kind.end_with?('RequiredRuleInfo')
97
+ attribute.unique = true if kind.end_with?('UniqueRuleInfo')
98
+ end
99
+ end
100
+
101
+ def self.normalize_indexes(indexes, attributes, qualified_name)
102
+ names_by_id = attributes.to_h { [_1.id, _1.name] }
103
+ indexes.each do |index|
104
+ members = IO::BsonCodec.parse_array(index['Attributes'])[:items]
105
+ members.each do |member|
106
+ next if member['Attribute'] || member['attribute']
107
+
108
+ id = IO::BsonCodec.extract_id(member['AttributePointer'])
109
+ name = names_by_id[id]
110
+ member['Attribute'] = [qualified_name, name].compact.join('.') if name
111
+ end
112
+ end
113
+ end
114
+
115
+ def self.parse_system_members(generalization)
116
+ return {} unless generalization.is_a?(Hash)
117
+
118
+ {
119
+ owner: generalization['hasOwner'] == true || generalization['HasOwnerAttr'] == true,
120
+ created_date: generalization.values_at(
121
+ 'hasCreatedDate', 'HasCreatedDateAttr'
122
+ ).include?(true),
123
+ changed_date: generalization.values_at(
124
+ 'hasChangedDate', 'HasChangedDateAttr'
125
+ ).include?(true),
126
+ changed_by: generalization.values_at(
127
+ 'hasChangedBy', 'HasChangedByAttr'
128
+ ).include?(true)
129
+ }
130
+ end
131
+
132
+ def self.parse_access_rule(doc)
133
+ roles = IO::BsonCodec.parse_array(doc["ModuleRoles"] || doc["AllowedModuleRoles"])[:items]
134
+ default_rights = doc["DefaultMemberAccessRights"] || "None"
135
+ member_items = IO::BsonCodec.parse_array(doc["MemberAccesses"])[:items]
136
+ members = member_items.map do |m|
137
+ association_ref = m["Association"].to_s
138
+ attr_ref = association_ref.empty? ? m["Attribute"] : association_ref
139
+ kind = association_ref.empty? ? :attribute : :association
140
+ { name: attr_ref.to_s.split(%r{[/.]}).last, reference: attr_ref.to_s,
141
+ rights: m["AccessRights"] || "None", kind: kind }
142
+ end
143
+ {
144
+ roles: roles,
145
+ create: doc["AllowCreate"] == true,
146
+ delete: doc["AllowDelete"] == true,
147
+ default_rights: default_rights,
148
+ members: members,
149
+ xpath: doc["XPathConstraint"] || ""
150
+ }
151
+ end
152
+
153
+ def self.parse_location(loc)
154
+ if loc.is_a?(String)
155
+ x, y = loc.split(';', 2).map(&:to_i)
156
+ return { x: x, y: y }
157
+ end
158
+ return { x: 0, y: 0 } unless loc.is_a?(Hash)
159
+ { x: loc["x"] || loc[:x] || 0, y: loc["y"] || loc[:y] || 0 }
160
+ end
161
+
162
+ def serialize_location(loc)
163
+ return { "x" => 0, "y" => 0 } unless loc
164
+ { "x" => loc[:x] || 0, "y" => loc[:y] || 0 }
165
+ end
166
+
167
+ def serialize_generalization
168
+ @generalization || {
169
+ "$ID" => SecureRandom.uuid,
170
+ "$Type" => "DomainModels$NoGeneralization",
171
+ "persistable" => @persistable != false,
172
+ "hasChangedDate" => false,
173
+ "hasCreatedDate" => false,
174
+ "hasOwner" => false,
175
+ "hasChangedBy" => false,
176
+ }
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "unit"
4
+
5
+ module Mxrb
6
+ module Model
7
+ class Menu < Unit
8
+ attr_reader :name, :items, :raw_document
9
+
10
+ def decode(doc)
11
+ @raw_document = doc
12
+ @name = doc["Name"]
13
+ collection = doc["ItemCollection"] || {}
14
+ @items = parse_array(collection["Items"]).map { menu_item(_1) }
15
+ end
16
+
17
+ private
18
+
19
+ def menu_item(doc)
20
+ {
21
+ name: doc["Name"],
22
+ caption: extract_text(doc["Caption"]),
23
+ page: doc.dig("Action", "FormSettings", "Form"),
24
+ items: parse_array(doc["Items"]).map { menu_item(_1) }
25
+ }.compact
26
+ end
27
+
28
+ def extract_text(obj)
29
+ return "" unless obj.is_a?(Hash)
30
+
31
+ translation = parse_array(obj["Items"] || obj["Translations"]).find { _1.is_a?(Hash) }
32
+ return "" unless translation
33
+
34
+ translation["Text"] || translation["Translation"] || ""
35
+ end
36
+ end
37
+ end
38
+ end