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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- 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
|