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,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Mxrb
|
|
6
|
+
module Compiler
|
|
7
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
8
|
+
# Compiles the built-in Data Grid 1 widget to the Dojo web-client contract.
|
|
9
|
+
class LegacyDataGridCompiler
|
|
10
|
+
include ModelValues
|
|
11
|
+
|
|
12
|
+
SOURCE_TYPES = {
|
|
13
|
+
'Forms$GridDatabaseSource' => 'database',
|
|
14
|
+
'Forms$NewGridDatabaseSource' => 'database',
|
|
15
|
+
'Forms$GridXPathSource' => 'xpath',
|
|
16
|
+
'Forms$MicroflowSource' => 'microflow'
|
|
17
|
+
}.freeze
|
|
18
|
+
BUTTON_FUNCTIONS = {
|
|
19
|
+
'Forms$GridSearchButton' => 'ToggleSearch',
|
|
20
|
+
'Forms$DataGridSelectButton' => 'ReturnSelection',
|
|
21
|
+
'Forms$GridNewButton' => 'InsertNew',
|
|
22
|
+
'Forms$GridEditButton' => 'EditSelection',
|
|
23
|
+
'Forms$GridDeleteButton' => 'DeleteSelection',
|
|
24
|
+
'Forms$GridActionButton' => 'InvokeAction'
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :unsupported
|
|
28
|
+
|
|
29
|
+
def initialize(source, page_name, widget, language:, sequence:)
|
|
30
|
+
@source = source
|
|
31
|
+
@page_name = page_name
|
|
32
|
+
@widget = widget
|
|
33
|
+
@language = language
|
|
34
|
+
@sequence = sequence
|
|
35
|
+
@unsupported = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def html
|
|
39
|
+
props = JSON.generate(properties)[1..-2]
|
|
40
|
+
attributes = {
|
|
41
|
+
'data-mendix-id' => widget_id, 'data-mendix-type' => 'mxui.widget.DataGrid',
|
|
42
|
+
'data-mendix-props' => props, 'class' => css_class, 'tabindex' => @widget['TabIndex'].to_i
|
|
43
|
+
}
|
|
44
|
+
"<div #{attributes.map { |key, value| "#{key}='#{escape(value)}'" }.join(' ')}></div>"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def properties
|
|
50
|
+
{
|
|
51
|
+
'friendlyId' => friendly_id, 'entity' => entity, 'schema' => entity_schema,
|
|
52
|
+
'config' => {
|
|
53
|
+
'locale' => @language, 'gridType' => 'Standalone',
|
|
54
|
+
'gridpresentation' => presentation, 'datasource' => datasource,
|
|
55
|
+
'searchOptions' => search_options, 'searchElements' => search_elements,
|
|
56
|
+
'controlBar' => control_bar, 'defaultButton' => default_button,
|
|
57
|
+
'griddata' => columns.map { compile_column(_1) }, 'plugins' => {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def presentation
|
|
63
|
+
{
|
|
64
|
+
'rows' => @widget['NumberOfRows'].to_i, 'columns' => 1,
|
|
65
|
+
'controlbar' => @widget['IsControlBarVisible'] == true,
|
|
66
|
+
'pagingbar' => @widget['IsPagingEnabled'] == true,
|
|
67
|
+
'sortparams' => sort_items, 'searchbar' => !search_elements.empty?,
|
|
68
|
+
'waitforsearch' => data_source.dig('SearchBar', 'WaitForSearch') == true,
|
|
69
|
+
'selectionmode' => @widget['SelectionMode'].to_s.downcase,
|
|
70
|
+
'editable' => columns.any? { _1['Editable'] == true }, 'sortable' => true,
|
|
71
|
+
'refresh' => @widget['RefreshTime'].to_i, 'selectfirst' => @widget['SelectFirst'] == true,
|
|
72
|
+
'showemptyrows' => @widget['ShowEmptyRows'] == true
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def datasource
|
|
77
|
+
type = SOURCE_TYPES[data_source['$Type']]
|
|
78
|
+
@unsupported << data_source['$Type'].to_s unless type
|
|
79
|
+
result = { 'friendlyId' => friendly_id, 'type' => type || 'unsupported', 'path' => entity }
|
|
80
|
+
constraint = data_source['XPathConstraint'].to_s
|
|
81
|
+
result['xpathConstraints'] = constraint if type == 'database' || !constraint.empty?
|
|
82
|
+
result['offlineConstraints'] = [] if type == 'database'
|
|
83
|
+
result['microflow'] = data_source.dig('MicroflowSettings', 'Microflow').to_s if type == 'microflow'
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def search_options
|
|
88
|
+
kind = data_source.dig('SearchBar', 'Type').to_s
|
|
89
|
+
{
|
|
90
|
+
'searchBar' => !search_elements.empty?, 'toggleable' => kind.start_with?('Foldable'),
|
|
91
|
+
'toggledByDefault' => kind != 'FoldableOpen'
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def search_elements
|
|
96
|
+
array(data_source.dig('SearchBar', 'NewButtons')).filter_map do |field|
|
|
97
|
+
unless %w[Forms$ComparisonSearchField Forms$DropDownSearchField].include?(field['$Type'])
|
|
98
|
+
@unsupported << field['$Type'].to_s
|
|
99
|
+
next
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
'searchInputName' => field['Name'].to_s, 'defaults' => default_search_value(field),
|
|
104
|
+
'defaultsParser' => 'Simple', 'caption' => translated(field['Caption']),
|
|
105
|
+
'path' => relative_attribute(attribute_path(field)),
|
|
106
|
+
'operator' => field.fetch('Operator', 'Equal').to_s.downcase
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def control_bar
|
|
112
|
+
tools = buttons.each_with_index.map { |button, index| compile_button(button, index) }
|
|
113
|
+
{
|
|
114
|
+
'gridTools' => tools.map(&:first),
|
|
115
|
+
'gridActions' => tools.filter_map(&:last)
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def compile_button(button, index)
|
|
120
|
+
id = "#{@sequence}_#{index}"
|
|
121
|
+
type = button['$Type'].to_s
|
|
122
|
+
function = BUTTON_FUNCTIONS[type]
|
|
123
|
+
@unsupported << type unless function
|
|
124
|
+
tool = {
|
|
125
|
+
'mxid' => id, 'friendlyId' => "#{@page_name}.#{button['Name']}",
|
|
126
|
+
'name' => button['Name'].to_s,
|
|
127
|
+
'caption' => { '$type' => 'textTemplate', 'text' => button_caption(button),
|
|
128
|
+
'friendlyId' => "#{@page_name}.#{button['Name']}" },
|
|
129
|
+
'title' => translated(button['Tooltip']), 'cssClass' => button_class(button)
|
|
130
|
+
}
|
|
131
|
+
action = function && { 'keyValue' => id, 'gridFunction' => function }
|
|
132
|
+
params = button_params(button)
|
|
133
|
+
action['params'] = params if action && params
|
|
134
|
+
[tool, action]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def button_params(button)
|
|
138
|
+
case button['$Type']
|
|
139
|
+
when 'Forms$GridNewButton', 'Forms$GridEditButton'
|
|
140
|
+
{ entity => { 'pageSettings' => page_settings(button['FormSettings']) } }
|
|
141
|
+
when 'Forms$GridActionButton'
|
|
142
|
+
{ entity => { 'action' => client_action(button['Action']),
|
|
143
|
+
'maintainSelection' => button['MaintainSelectionAfterMicroflow'] == true } }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def client_action(action)
|
|
148
|
+
case action&.fetch('$Type', nil)
|
|
149
|
+
when 'Forms$MicroflowAction'
|
|
150
|
+
microflow = action.dig('MicroflowSettings', 'Microflow').to_s
|
|
151
|
+
{ 'type' => 'callMicroflow', 'hasParameter' => true,
|
|
152
|
+
'params' => { 'name' => microflow, 'validate' => 'view', 'applyTo' => 'selection' } }
|
|
153
|
+
when 'Forms$FormAction'
|
|
154
|
+
{ 'type' => 'openPage', 'params' => page_settings(action['FormSettings']) }
|
|
155
|
+
when 'Forms$DeleteClientAction'
|
|
156
|
+
{ 'type' => 'deleteObject', 'params' => { 'applyTo' => 'selection' } }
|
|
157
|
+
when 'Forms$NoAction', nil then { 'type' => 'noAction' }
|
|
158
|
+
else
|
|
159
|
+
@unsupported << action['$Type'].to_s
|
|
160
|
+
{ 'type' => 'unsupported' }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def page_settings(settings)
|
|
165
|
+
form = settings&.fetch('Form', '').to_s
|
|
166
|
+
location = settings&.fetch('Location', '').to_s
|
|
167
|
+
result = {
|
|
168
|
+
'path' => "#{form.tr('.', '/')}.page.xml",
|
|
169
|
+
'location' => %w[Popup ModalPopup].include?(location) ? 'modal' : 'content'
|
|
170
|
+
}
|
|
171
|
+
target = @source.units_of('Forms$Page').find do |unit|
|
|
172
|
+
"#{unit.module_name}.#{unit.document['Name']}" == form
|
|
173
|
+
end
|
|
174
|
+
result['resizable'] = true if target&.document&.fetch('PopupResizable', false) == true
|
|
175
|
+
result
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def default_button
|
|
179
|
+
pointer = IO::BsonCodec.extract_id(@widget.dig('ControlBar', 'DefaultButtonPointer'))
|
|
180
|
+
index = buttons.index { IO::BsonCodec.extract_id(_1['$ID']) == pointer }
|
|
181
|
+
return nil unless index
|
|
182
|
+
|
|
183
|
+
{ 'clickType' => @widget['DefaultButtonTrigger'].to_s.downcase, 'mxid' => "#{@sequence}_#{index}" }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def compile_column(column)
|
|
187
|
+
path = relative_attribute(attribute_path(column))
|
|
188
|
+
{
|
|
189
|
+
'tag' => path, 'name' => column['Name'].to_s, 'editable' => column['Editable'] == true,
|
|
190
|
+
'render' => render_type(column),
|
|
191
|
+
'display' => { 'width' => "#{column['WidthValue'].to_i}%", 'string' => translated(column['Caption']) }
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def sort_items
|
|
196
|
+
array(data_source.dig('SortBar', 'SortItems')).map do |item|
|
|
197
|
+
[relative_attribute(attribute_path(item)), item['SortOrder'].to_s.downcase.sub('ending', '')]
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def entity
|
|
202
|
+
direct = data_source['EntityPath'] || data_source.dig('EntityRef', 'Entity')
|
|
203
|
+
direct.to_s.empty? ? attribute_path(columns.first).to_s.rpartition('.').first : direct.to_s
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def entity_schema
|
|
207
|
+
IO::BsonCodec.extract_id(@widget['$ID']).to_s
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def render_type(column)
|
|
211
|
+
format = column.dig('FormattingInfo', 'EnumFormat')
|
|
212
|
+
format == 'Image' ? 'EnumImage' : 'String'
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def attribute_path(value)
|
|
216
|
+
value['AttributePath'] || value.dig('AttributeRef', 'Attribute')
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def relative_attribute(path)
|
|
220
|
+
path.to_s.delete_prefix("#{entity}.")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def buttons
|
|
224
|
+
@buttons ||= begin
|
|
225
|
+
control = @widget['ControlBar'] || {}
|
|
226
|
+
[control['SearchButton'], *array(control['NewButtons'])].compact
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def data_source = @widget['DataSource'] || {}
|
|
231
|
+
def columns = array(@widget['Columns'])
|
|
232
|
+
def friendly_id = "#{@page_name}.#{@widget['Name']}"
|
|
233
|
+
def widget_id = "#{@sequence}_#{buttons.length}"
|
|
234
|
+
|
|
235
|
+
def css_class
|
|
236
|
+
['mx-datagrid', "mx-name-#{@widget['Name']}", @widget['Class']]
|
|
237
|
+
.map(&:to_s).reject(&:empty?).uniq.join(' ')
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def translated(text)
|
|
241
|
+
items = array(text&.fetch('Items', nil))
|
|
242
|
+
items.find { _1['LanguageCode'] == @language }&.fetch('Text', '') ||
|
|
243
|
+
items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') ||
|
|
244
|
+
items.first&.fetch('Text', '') || ''
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def button_caption(button)
|
|
248
|
+
translated(button.dig('CaptionTemplate', 'Template'))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def button_class(button)
|
|
252
|
+
style = button['ButtonStyle'].to_s.downcase
|
|
253
|
+
"btn-#{style.empty? ? 'default' : style}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def default_search_value(field)
|
|
257
|
+
value = field['DefaultValue']
|
|
258
|
+
value.nil? || value.to_s.empty? ? nil : value
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def escape(value)
|
|
262
|
+
value.to_s.gsub('&', '&').gsub("'", ''').gsub('<', '<').gsub('>', '>')
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Mxrb
|
|
7
|
+
module Compiler
|
|
8
|
+
LegacyPageBuild = Data.define(:directory, :files, :bytes, :unsupported_widgets)
|
|
9
|
+
|
|
10
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
11
|
+
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
12
|
+
# Writes deterministic Dojo page XML for the built-in Data Grid 1 contract.
|
|
13
|
+
class LegacyPageBuilder
|
|
14
|
+
include ModelValues
|
|
15
|
+
|
|
16
|
+
# Model elements that own visible client widgets. Supporting a page means
|
|
17
|
+
# rendering these elements, not merely serializing their nested settings.
|
|
18
|
+
VISUAL_WIDGET_TYPES = %w[
|
|
19
|
+
Forms$ActionButton Forms$CheckBox Forms$DataGrid Forms$DataView Forms$DatePicker
|
|
20
|
+
Forms$DivContainer Forms$DropDown Forms$DynamicText Forms$ImageViewer Forms$Label
|
|
21
|
+
Forms$LayoutGrid Forms$ListView Forms$RadioButtonGroup Forms$ReferenceSelector
|
|
22
|
+
Forms$SnippetCallWidget Forms$StaticImageViewer Forms$TabControl Forms$TextArea
|
|
23
|
+
Forms$TextBox Forms$InputReferenceSetSelector CustomWidgets$CustomWidget
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
def initialize(source, web_root, profiles: [:dojo])
|
|
27
|
+
@source = source
|
|
28
|
+
@web_root = web_root
|
|
29
|
+
@profiles = profiles
|
|
30
|
+
@unsupported = Hash.new { |hash, key| hash[key] = [] }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build
|
|
34
|
+
pages = @source.units_of('Forms$Page')
|
|
35
|
+
layouts = @source.units_of('Forms$Layout')
|
|
36
|
+
languages.each do |language|
|
|
37
|
+
layouts.each { write_layout(_1, language) }
|
|
38
|
+
pages.each { write_page(_1, language) }
|
|
39
|
+
end
|
|
40
|
+
write_manifest
|
|
41
|
+
files = Dir.glob(File.join(@web_root, 'pages', '**', '*.xml')).select { File.file?(_1) }
|
|
42
|
+
LegacyPageBuild.new(
|
|
43
|
+
directory: File.join(@web_root, 'pages'), files: files.length,
|
|
44
|
+
bytes: files.sum { File.size(_1) }, unsupported_widgets: frozen_unsupported
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Performs the same widget audit as #build without writing deployment files.
|
|
49
|
+
def audit
|
|
50
|
+
@unsupported.clear
|
|
51
|
+
(@source.units_of('Forms$Layout') + @source.units_of('Forms$Page')).each do |unit|
|
|
52
|
+
name = "#{unit.module_name}.#{unit.document['Name']}"
|
|
53
|
+
audit_unsupported_widgets(unit.document, name)
|
|
54
|
+
descendants(unit.document).select { _1['$Type'] == 'Forms$DataGrid' }.each_with_index do |grid, index|
|
|
55
|
+
compiler = LegacyDataGridCompiler.new(@source, name, grid, language: 'en_US', sequence: index + 1)
|
|
56
|
+
compiler.html
|
|
57
|
+
@unsupported[name].concat(compiler.unsupported)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
frozen_unsupported
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def write_page(unit, language)
|
|
66
|
+
name = "#{unit.module_name}.#{unit.document['Name']}"
|
|
67
|
+
audit_unsupported_widgets(unit.document, name)
|
|
68
|
+
@widget_sequence = 0
|
|
69
|
+
rendered = page_widgets(unit.document).map { render_widget(_1, name, language) }.join
|
|
70
|
+
path = File.join(@web_root, 'pages', language, unit.module_name, "#{unit.document['Name']}.page.xml")
|
|
71
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
72
|
+
File.binwrite(path, page_xml(unit, language, rendered))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def write_layout(unit, language)
|
|
76
|
+
name = "#{unit.module_name}.#{unit.document['Name']}"
|
|
77
|
+
audit_unsupported_widgets(unit.document, name)
|
|
78
|
+
@widget_sequence = 0
|
|
79
|
+
rendered = layout_widgets(unit.document).map { render_widget(_1, name, language) }.join
|
|
80
|
+
path = File.join(@web_root, 'pages', language, unit.module_name, "#{unit.document['Name']}.layout.xml")
|
|
81
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
82
|
+
File.binwrite(path, layout_xml(unit, rendered))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def audit_unsupported_widgets(root, page_name)
|
|
86
|
+
visit = lambda do |value|
|
|
87
|
+
case value
|
|
88
|
+
when Hash
|
|
89
|
+
type = value['$Type'].to_s
|
|
90
|
+
if type == 'Forms$DataGrid'
|
|
91
|
+
return
|
|
92
|
+
elsif VISUAL_WIDGET_TYPES.include?(type) && !supported_widget_type?(type)
|
|
93
|
+
@unsupported[page_name] << type
|
|
94
|
+
elsif type == 'Forms$DynamicText' && !array(value.dig('Content', 'Parameters')).empty?
|
|
95
|
+
@unsupported[page_name] << 'Forms$DynamicText(parameters)'
|
|
96
|
+
elsif type == 'Forms$DivContainer'
|
|
97
|
+
action_type = value.dig('OnClickAction', '$Type').to_s
|
|
98
|
+
@unsupported[page_name] << 'Forms$DivContainer(onClick)' \
|
|
99
|
+
unless action_type.empty? || action_type == 'Forms$NoAction'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
value.each_value { visit.call(_1) }
|
|
103
|
+
when Array then value.each { visit.call(_1) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
visit.call(root)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def page_xml(unit, language, content)
|
|
110
|
+
page = unit.document
|
|
111
|
+
title = translated(page['Title'], language)
|
|
112
|
+
layout = page.dig('FormCall', 'Form').to_s.tr('.', '/')
|
|
113
|
+
argument = array(page.dig('FormCall', 'Arguments')).first
|
|
114
|
+
parameter = layout_argument_id(argument&.fetch('Parameter', '').to_s)
|
|
115
|
+
css = page_css_classes(page)
|
|
116
|
+
class_attribute = css.empty? ? '' : " class='#{escape(css)}'"
|
|
117
|
+
"\uFEFF<?xml version='1.0' encoding='utf-8'?>" \
|
|
118
|
+
"<m:page id='#{unit.id}' xmlns='http://www.w3.org/1999/xhtml' " \
|
|
119
|
+
"title='#{escape(title)}'#{class_attribute} xmlns:m='http://schemas.mendix.com/forms/1.0'>" \
|
|
120
|
+
"<m:layouts>#{layout.empty? ? '' : "<m:layout path='#{escape(layout)}.layout.xml'></m:layout>"}</m:layouts>" \
|
|
121
|
+
"<m:arguments><m:argument parameterName='#{escape(parameter)}'>#{content}</m:argument></m:arguments>" \
|
|
122
|
+
'</m:page>'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def layout_xml(unit, content)
|
|
126
|
+
"\uFEFF<?xml version='1.0' encoding='utf-8'?>" \
|
|
127
|
+
"<m:layout id='#{unit.id}' xmlns='http://www.w3.org/1999/xhtml' " \
|
|
128
|
+
"xmlns:m='http://schemas.mendix.com/forms/1.0'>" \
|
|
129
|
+
"<m:arguments><m:argument>#{content}</m:argument></m:arguments></m:layout>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def render_widget(widget, page_name, language)
|
|
133
|
+
return '' unless widget.is_a?(Hash)
|
|
134
|
+
|
|
135
|
+
case widget['$Type']
|
|
136
|
+
when 'Forms$VerticalFlow'
|
|
137
|
+
widget_children(widget).map { render_widget(_1, page_name, language) }.join
|
|
138
|
+
when 'Forms$Placeholder'
|
|
139
|
+
identifier = IO::BsonCodec.extract_id(widget['$ID']) || widget['Name'].to_s
|
|
140
|
+
"<div data-mx-placeholder='#{escape(identifier)}' class='mx-placeholder'></div>"
|
|
141
|
+
when 'Forms$DivContainer'
|
|
142
|
+
render_container(widget, page_name, language)
|
|
143
|
+
when 'Forms$DynamicText'
|
|
144
|
+
render_dynamic_text(widget, language)
|
|
145
|
+
when 'Forms$DataGrid'
|
|
146
|
+
render_data_grid(widget, page_name, language)
|
|
147
|
+
else
|
|
148
|
+
''
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def render_container(widget, page_name, language)
|
|
153
|
+
children = widget_children(widget).map { render_widget(_1, page_name, language) }.join
|
|
154
|
+
tag = widget['RenderMode'].to_s == 'Span' ? 'span' : 'div'
|
|
155
|
+
"<#{tag}#{html_attributes(widget, base: nil)}>#{children}</#{tag}>"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def render_dynamic_text(widget, language)
|
|
159
|
+
content = widget['Content'] || {}
|
|
160
|
+
text = translated(content['Template'], language)
|
|
161
|
+
text = translated(content['Fallback'], language) if text.empty?
|
|
162
|
+
tag = widget['RenderMode'].to_s == 'Paragraph' ? 'p' : 'span'
|
|
163
|
+
attributes = html_attributes(widget, base: 'mx-text', mendix_id: next_widget_id)
|
|
164
|
+
"<#{tag}#{attributes}>#{escape(text)}</#{tag}>"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def render_data_grid(widget, page_name, language)
|
|
168
|
+
compiler = LegacyDataGridCompiler.new(
|
|
169
|
+
@source, page_name, widget, language:, sequence: next_widget_sequence
|
|
170
|
+
)
|
|
171
|
+
compiler.html.tap { @unsupported[page_name].concat(compiler.unsupported) }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def html_attributes(widget, base:, mendix_id: nil)
|
|
175
|
+
values = {}
|
|
176
|
+
css = css_classes(widget, base:)
|
|
177
|
+
values['class'] = css unless css.empty?
|
|
178
|
+
style = widget['Style'].to_s
|
|
179
|
+
style = widget.dig('Appearance', 'Style').to_s if style.empty?
|
|
180
|
+
values['style'] = style unless style.empty?
|
|
181
|
+
values['data-mendix-id'] = mendix_id if mendix_id
|
|
182
|
+
values.map { |key, value| " #{key}='#{escape(value)}'" }.join
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def css_classes(widget, base:)
|
|
186
|
+
name = widget['Name'].to_s
|
|
187
|
+
classes = [base, ("mx-name-#{name}" unless name.empty?), widget['Class'],
|
|
188
|
+
widget.dig('Appearance', 'Class')]
|
|
189
|
+
classes.compact.flat_map { _1.to_s.split }.reject(&:empty?).uniq.join(' ')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def page_css_classes(page)
|
|
193
|
+
layout = layout_unit(page.dig('FormCall', 'Form').to_s)&.document || {}
|
|
194
|
+
[layout['Class'], layout.dig('Appearance', 'Class'), page['Class'],
|
|
195
|
+
page.dig('Appearance', 'Class')].compact.flat_map { _1.to_s.split }
|
|
196
|
+
.reject(&:empty?).uniq.join(' ')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def page_widgets(document)
|
|
200
|
+
argument = array(document.dig('FormCall', 'Arguments')).first
|
|
201
|
+
widget_children(argument || document)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def layout_widgets(document) = widget_children(document)
|
|
205
|
+
|
|
206
|
+
def widget_children(value)
|
|
207
|
+
return [] unless value.is_a?(Hash)
|
|
208
|
+
|
|
209
|
+
plural = array(value['Widgets'])
|
|
210
|
+
singular = value['Widget']
|
|
211
|
+
plural.empty? && singular.is_a?(Hash) ? [singular] : plural
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def supported_widget_type?(type)
|
|
215
|
+
%w[Forms$DataGrid Forms$DivContainer Forms$DynamicText].include?(type)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def layout_argument_id(parameter)
|
|
219
|
+
module_name, layout_name, placeholder_name = parameter.split('.', 3)
|
|
220
|
+
return parameter unless placeholder_name
|
|
221
|
+
|
|
222
|
+
layout = layout_unit("#{module_name}.#{layout_name}")
|
|
223
|
+
placeholder = descendants(layout&.document || {}).find do |value|
|
|
224
|
+
value['$Type'] == 'Forms$Placeholder' && value['Name'] == placeholder_name
|
|
225
|
+
end
|
|
226
|
+
IO::BsonCodec.extract_id(placeholder&.fetch('$ID', nil)) || parameter
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def layout_unit(qualified_name)
|
|
230
|
+
module_name, layout_name = qualified_name.split('.', 2)
|
|
231
|
+
@source.units_of('Forms$Layout').find do |unit|
|
|
232
|
+
unit.module_name == module_name && unit.document['Name'] == layout_name
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def next_widget_sequence
|
|
237
|
+
@widget_sequence += 1
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def next_widget_id = "1_#{next_widget_sequence - 1}"
|
|
241
|
+
|
|
242
|
+
def descendants(root)
|
|
243
|
+
result = []
|
|
244
|
+
visit = lambda do |value|
|
|
245
|
+
case value
|
|
246
|
+
when Hash
|
|
247
|
+
result << value if value['$Type']
|
|
248
|
+
value.each_value { visit.call(_1) }
|
|
249
|
+
when Array then value.each { visit.call(_1) }
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
visit.call(root)
|
|
253
|
+
result
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def languages
|
|
257
|
+
found = @source.documents.flat_map do |document|
|
|
258
|
+
descendants(document).filter_map do |value|
|
|
259
|
+
value['LanguageCode'].to_s if value['$Type'] == 'Texts$Translation'
|
|
260
|
+
end
|
|
261
|
+
end.reject(&:empty?).uniq.sort
|
|
262
|
+
found.empty? ? ['en_US'] : found
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def translated(text, language)
|
|
266
|
+
items = array(text&.fetch('Items', nil))
|
|
267
|
+
items.find { _1['LanguageCode'] == language }&.fetch('Text', '') ||
|
|
268
|
+
items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') || ''
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def write_manifest
|
|
272
|
+
manifest = { 'profiles' => @profiles.map(&:to_s), 'unsupportedWidgets' => frozen_unsupported }
|
|
273
|
+
File.write(File.join(@web_root, 'mxrb-legacy-pages.json'), JSON.pretty_generate(manifest))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def frozen_unsupported
|
|
277
|
+
@unsupported.transform_values { _1.reject(&:empty?).uniq.sort }.freeze
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def escape(value)
|
|
281
|
+
value.to_s.gsub('&', '&').gsub("'", ''').gsub('<', '<').gsub('>', '>')
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
285
|
+
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'zip'
|
|
6
|
+
|
|
7
|
+
module Mxrb
|
|
8
|
+
module Compiler
|
|
9
|
+
# Safe inspection and content-level comparison of Mendix deployment archives.
|
|
10
|
+
module Mda
|
|
11
|
+
Entry = Data.define(:path, :size, :crc, :sha256, :directory)
|
|
12
|
+
Inspection = Data.define(:path, :metadata, :entries, :sha256) do
|
|
13
|
+
def files = entries.reject(&:directory)
|
|
14
|
+
def roots = entries.map { _1.path.split('/').first }.uniq.sort
|
|
15
|
+
end
|
|
16
|
+
Difference = Data.define(:path, :status, :left_sha256, :right_sha256)
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def inspect(path)
|
|
21
|
+
archive_path = File.expand_path(path)
|
|
22
|
+
entries = read_entries(archive_path)
|
|
23
|
+
metadata_entry = entries.find { _1.path == 'model/metadata.json' }
|
|
24
|
+
metadata = read_metadata(archive_path, metadata_entry)
|
|
25
|
+
raise CompilationError, 'MDA has no model/metadata.json' unless metadata
|
|
26
|
+
|
|
27
|
+
build_inspection(archive_path, metadata, entries)
|
|
28
|
+
rescue Zip::Error, JSON::ParserError => e
|
|
29
|
+
raise CompilationError, "invalid MDA #{archive_path}: #{e.message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def compare(left, right)
|
|
33
|
+
left_entries = entry_map(left)
|
|
34
|
+
right_entries = entry_map(right)
|
|
35
|
+
(left_entries.keys | right_entries.keys).sort.filter_map do |path|
|
|
36
|
+
lhs = left_entries[path]
|
|
37
|
+
rhs = right_entries[path]
|
|
38
|
+
status = difference_status(lhs, rhs)
|
|
39
|
+
next unless status
|
|
40
|
+
|
|
41
|
+
Difference.new(path:, status:, left_sha256: lhs&.sha256, right_sha256: rhs&.sha256)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def read_entries(archive_path)
|
|
46
|
+
Zip::File.open(archive_path) do |archive|
|
|
47
|
+
archive.map { build_entry(_1) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_inspection(path, metadata, entries)
|
|
52
|
+
Inspection.new(
|
|
53
|
+
path:, metadata:, entries: entries.sort_by(&:path),
|
|
54
|
+
sha256: Digest::SHA256.file(path).hexdigest
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_entry(entry)
|
|
59
|
+
path = safe_path(entry.name)
|
|
60
|
+
directory = entry.directory?
|
|
61
|
+
bytes = directory ? '' : entry.get_input_stream.read
|
|
62
|
+
Entry.new(
|
|
63
|
+
path:, size: entry.size, crc: entry.crc,
|
|
64
|
+
sha256: directory ? nil : Digest::SHA256.hexdigest(bytes), directory:
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read_metadata(archive_path, entry)
|
|
69
|
+
return unless entry
|
|
70
|
+
|
|
71
|
+
Zip::File.open(archive_path) do |archive|
|
|
72
|
+
JSON.parse(archive.get_entry(entry.path).get_input_stream.read)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def entry_map(path)
|
|
77
|
+
inspect(path).files.to_h { [_1.path, _1] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def difference_status(left, right)
|
|
81
|
+
return :added unless left
|
|
82
|
+
return :removed unless right
|
|
83
|
+
return :changed if left.sha256 != right.sha256
|
|
84
|
+
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def safe_path(path)
|
|
89
|
+
clean = path.to_s.tr('\\', '/').sub(%r{\A/+}, '')
|
|
90
|
+
parts = clean.split('/')
|
|
91
|
+
if clean.empty? || parts.include?('..') || parts.include?('.')
|
|
92
|
+
raise CompilationError, "unsafe MDA entry #{path.inspect}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
clean
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|