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,775 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Mxrb
|
|
7
|
+
module Frontend
|
|
8
|
+
MigrationIssue = Data.define(:unit_id, :path, :kind, :message)
|
|
9
|
+
MigrationChange = Data.define(
|
|
10
|
+
:unit_id, :before_hash, :before, :after, :widgets, :layout_rows, :design_properties
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Immutable, fail-closed preview of frontend model migrations.
|
|
14
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
15
|
+
class MigrationPlan
|
|
16
|
+
attr_reader :path, :version, :changes, :issues
|
|
17
|
+
|
|
18
|
+
def initialize(path:, version:, changes:, issues:)
|
|
19
|
+
@path = path
|
|
20
|
+
@version = version
|
|
21
|
+
@changes = changes.freeze
|
|
22
|
+
@issues = issues.freeze
|
|
23
|
+
@applied = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def safe? = issues.empty?
|
|
27
|
+
def applied? = @applied
|
|
28
|
+
def widgets = changes.sum(&:widgets)
|
|
29
|
+
def layout_rows = changes.sum(&:layout_rows)
|
|
30
|
+
def design_properties = changes.sum(&:design_properties)
|
|
31
|
+
|
|
32
|
+
def apply!
|
|
33
|
+
raise SerializationError, blocked_message unless safe?
|
|
34
|
+
raise SerializationError, 'frontend migration plan was already applied' if applied?
|
|
35
|
+
|
|
36
|
+
mpr = IO::MprFile.open(path)
|
|
37
|
+
mpr.transaction do
|
|
38
|
+
changes.each do |change|
|
|
39
|
+
current = mpr.unit(change.unit_id)
|
|
40
|
+
raise SerializationError, "frontend unit disappeared: #{change.unit_id}" unless current
|
|
41
|
+
raise SerializationError, "frontend unit changed after preview: #{change.unit_id}" unless
|
|
42
|
+
current['ContentsHash'] == change.before_hash
|
|
43
|
+
|
|
44
|
+
mpr.update_unit(change.unit_id, change.after)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
@applied = true
|
|
48
|
+
self
|
|
49
|
+
ensure
|
|
50
|
+
mpr&.close
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def blocked_message
|
|
56
|
+
details = issues.map { "#{_1.kind} at #{_1.unit_id}#{_1.path}: #{_1.message}" }
|
|
57
|
+
"frontend migration is blocked: #{details.join('; ')}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
61
|
+
|
|
62
|
+
# Updates installed pluggable-widget schemas and legacy layout-grid weights
|
|
63
|
+
# directly in an MPR. Package XML is the source of truth; no Studio Pro or
|
|
64
|
+
# mx invocation is involved.
|
|
65
|
+
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength
|
|
66
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
67
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
68
|
+
# rubocop:disable Lint/NonLocalExitFromIterator
|
|
69
|
+
class Migrator
|
|
70
|
+
SUPPORTED_MAJORS = [10, 11].freeze
|
|
71
|
+
STANDARD_TYPE_KEYS = %w[
|
|
72
|
+
$ID $Type HelpUrl ObjectType OfflineCapable StudioCategory StudioProCategory
|
|
73
|
+
SupportedPlatform WidgetDescription WidgetId WidgetName WidgetNeedsEntityContext
|
|
74
|
+
WidgetPluginWidget
|
|
75
|
+
].freeze
|
|
76
|
+
STANDARD_OBJECT_KEYS = %w[$ID $Type Properties TypePointer].freeze
|
|
77
|
+
# TabletWeight/PhoneWeight use -1 as a valid responsive inheritance
|
|
78
|
+
# sentinel. CE0535 concerns only the desktop Weight total.
|
|
79
|
+
WEIGHT_KEYS = %w[Weight].freeze
|
|
80
|
+
AUDITED_DATA_GRID_PACKAGE_SHA256 =
|
|
81
|
+
'8eabd99ef927b918dbb2bd5f05cf12dc14026b37e496bc1b0569c34369a0b713'
|
|
82
|
+
ACTIVE_VALUE_FIELDS = {
|
|
83
|
+
'Action' => %w[Action Microflow Nanoflow],
|
|
84
|
+
'Association' => %w[EntityRef SourceVariable],
|
|
85
|
+
'Attribute' => %w[AttributeRef SourceVariable],
|
|
86
|
+
'DataSource' => %w[DataSource XPathConstraint],
|
|
87
|
+
'Expression' => %w[Expression SourceVariable], 'Icon' => %w[Icon],
|
|
88
|
+
'Image' => %w[Image], 'Object' => %w[Objects], 'Selection' => %w[Selection],
|
|
89
|
+
'TextTemplate' => %w[TextTemplate TranslatableValue], 'Widgets' => %w[Widgets]
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
def self.plan(path) = new(path).plan
|
|
93
|
+
|
|
94
|
+
def initialize(path)
|
|
95
|
+
@path = File.expand_path(path)
|
|
96
|
+
@root = File.dirname(@path)
|
|
97
|
+
@definitions = {}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def plan
|
|
101
|
+
mpr = IO::MprFile.open(@path, readonly: true)
|
|
102
|
+
version = mpr.mendix_version.to_s
|
|
103
|
+
issues = []
|
|
104
|
+
unless SUPPORTED_MAJORS.include?(version.to_i)
|
|
105
|
+
issues << MigrationIssue.new('', '', :unsupported_version,
|
|
106
|
+
"Mendix #{version.inspect}; supported majors are 10 and 11")
|
|
107
|
+
return MigrationPlan.new(path: @path, version:, changes: [], issues:)
|
|
108
|
+
end
|
|
109
|
+
validate_widget_packages!(issues)
|
|
110
|
+
|
|
111
|
+
changes = mpr.all_units.filter_map do |unit|
|
|
112
|
+
before = mpr.parse_contents(unit)
|
|
113
|
+
after = deep_copy(before)
|
|
114
|
+
counts = { widgets: 0, layout_rows: 0, design_properties: 0 }
|
|
115
|
+
migrate_value!(after, unit.fetch('UnitID'), '$', issues, counts)
|
|
116
|
+
next if after == before
|
|
117
|
+
|
|
118
|
+
MigrationChange.new(
|
|
119
|
+
unit.fetch('UnitID'), unit.fetch('ContentsHash'), before, after,
|
|
120
|
+
counts.fetch(:widgets), counts.fetch(:layout_rows), counts.fetch(:design_properties)
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
MigrationPlan.new(path: @path, version:, changes:, issues:)
|
|
124
|
+
ensure
|
|
125
|
+
mpr&.close
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def migrate_value!(value, unit_id, path, issues, counts)
|
|
131
|
+
case value
|
|
132
|
+
when Hash
|
|
133
|
+
migrate_design_properties!(value, unit_id, path, issues, counts) if value.key?('DesignProperties')
|
|
134
|
+
migrate_layout_row!(value, unit_id, path, issues, counts) if layout_row?(value)
|
|
135
|
+
migrate_widget!(value, unit_id, path, issues, counts) if custom_widget?(value)
|
|
136
|
+
value.each { |key, child| migrate_value!(child, unit_id, "#{path}.#{key}", issues, counts) }
|
|
137
|
+
when Array
|
|
138
|
+
value.each_with_index do |child, index|
|
|
139
|
+
migrate_value!(child, unit_id, "#{path}[#{index}]", issues, counts)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def migrate_widget!(widget, unit_id, path, issues, counts)
|
|
145
|
+
widget_id = widget.dig('Type', 'WidgetId').to_s
|
|
146
|
+
return if widget_id.empty?
|
|
147
|
+
|
|
148
|
+
definition = definition(widget_id)
|
|
149
|
+
unless definition
|
|
150
|
+
kind, message = definition_failure(widget_id)
|
|
151
|
+
return issue(issues, unit_id, path, kind, message)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
new_type, new_object = WidgetPackage.template(definition)
|
|
155
|
+
return unless safe_envelope?(widget, new_type, new_object, unit_id, path, issues)
|
|
156
|
+
|
|
157
|
+
migrated = rebind_object(
|
|
158
|
+
widget['Object'], widget.dig('Type', 'ObjectType'),
|
|
159
|
+
new_object, new_type['ObjectType'], unit_id, path, issues
|
|
160
|
+
)
|
|
161
|
+
return unless migrated
|
|
162
|
+
|
|
163
|
+
apply_audited_package_migration!(migrated, new_type, package_digest(widget_id))
|
|
164
|
+
schema_changed = canonical_schema(widget['Type']) != canonical_schema(new_type)
|
|
165
|
+
object_changed = canonical_schema(widget['Object']) != canonical_schema(migrated)
|
|
166
|
+
return unless schema_changed || object_changed
|
|
167
|
+
|
|
168
|
+
widget['Type'] = new_type
|
|
169
|
+
widget['Object'] = migrated
|
|
170
|
+
counts[:widgets] += 1
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def safe_envelope?(widget, new_type, new_object, unit_id, path, issues)
|
|
174
|
+
old_type = widget['Type']
|
|
175
|
+
old_object = widget['Object']
|
|
176
|
+
return issue(issues, unit_id, path, :malformed_widget, 'missing Type or Object') unless
|
|
177
|
+
old_type.is_a?(Hash) && old_object.is_a?(Hash)
|
|
178
|
+
|
|
179
|
+
extra_type = old_type.keys - STANDARD_TYPE_KEYS - new_type.keys
|
|
180
|
+
extra_object = old_object.keys - STANDARD_OBJECT_KEYS - new_object.keys
|
|
181
|
+
if extra_type.any?
|
|
182
|
+
return issue(issues, unit_id, path, :unknown_widget_schema,
|
|
183
|
+
"unrecognized Type fields: #{extra_type.sort.join(', ')}")
|
|
184
|
+
end
|
|
185
|
+
if extra_object.any?
|
|
186
|
+
return issue(issues, unit_id, path, :unknown_widget_object,
|
|
187
|
+
"unrecognized Object fields: #{extra_object.sort.join(', ')}")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def rebind_object(old_object, old_type, new_object, new_type, unit_id, path, issues)
|
|
194
|
+
old_types = property_types(old_type)
|
|
195
|
+
new_types = property_types(new_type)
|
|
196
|
+
old_by_pointer = old_types.to_h { [id(_1['$ID']), _1] }
|
|
197
|
+
new_by_key = new_types.to_h { [_1['PropertyKey'].to_s, _1] }
|
|
198
|
+
old_values = property_values(old_object)
|
|
199
|
+
rebound = {}
|
|
200
|
+
|
|
201
|
+
old_values.each do |property|
|
|
202
|
+
old_property_type = old_by_pointer[id(property['TypePointer'])]
|
|
203
|
+
unless old_property_type
|
|
204
|
+
return issue(issues, unit_id, path, :unknown_property_pointer,
|
|
205
|
+
"property pointer #{id(property['TypePointer']).inspect} is not in WidgetType")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
key = old_property_type['PropertyKey'].to_s
|
|
209
|
+
new_property_type = new_by_key[key]
|
|
210
|
+
old_value_type = old_property_type['ValueType'] || {}
|
|
211
|
+
unless new_property_type
|
|
212
|
+
next if default_widget_value?(property['Value'], old_value_type)
|
|
213
|
+
|
|
214
|
+
return issue(issues, unit_id, path, :removed_configured_widget_property,
|
|
215
|
+
"installed schema removed configured property #{key.inspect}")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
new_value_type = new_property_type['ValueType'] || {}
|
|
219
|
+
value = if old_value_type['Type'] == new_value_type['Type']
|
|
220
|
+
rebind_value(
|
|
221
|
+
property['Value'], old_value_type, new_value_type,
|
|
222
|
+
unit_id, "#{path}.#{key}", issues
|
|
223
|
+
)
|
|
224
|
+
else
|
|
225
|
+
convert_value(
|
|
226
|
+
property['Value'], old_value_type, new_value_type,
|
|
227
|
+
unit_id, "#{path}.#{key}", issues
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
return unless value
|
|
231
|
+
|
|
232
|
+
rebound[key] = deep_copy(property).merge(
|
|
233
|
+
'TypePointer' => new_property_type['$ID'], 'Value' => value
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
result = deep_copy(new_object)
|
|
238
|
+
result['$ID'] = old_object['$ID'] if old_object.key?('$ID')
|
|
239
|
+
defaults = property_values(new_object).to_h do |property|
|
|
240
|
+
type = new_types.find { id(_1['$ID']) == id(property['TypePointer']) }
|
|
241
|
+
property = neutral_new_property(property, type)
|
|
242
|
+
[type.fetch('PropertyKey').to_s, property]
|
|
243
|
+
end
|
|
244
|
+
clear_inactive_default_captions!(rebound, new_by_key)
|
|
245
|
+
added = new_types.filter_map do |type|
|
|
246
|
+
key = type['PropertyKey'].to_s
|
|
247
|
+
defaults[key] unless rebound.key?(key)
|
|
248
|
+
end
|
|
249
|
+
# Studio Pro preserves the previous object-property order and appends
|
|
250
|
+
# newly introduced properties, even when their schema declaration is
|
|
251
|
+
# located in the middle of the XML. CE0463 checks that storage order.
|
|
252
|
+
values = rebound.values + added
|
|
253
|
+
result['Properties'] = IO::BsonCodec.build_array(values, marker: array_marker(new_object['Properties']))
|
|
254
|
+
result
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def neutral_new_property(property, _type) = deep_copy(property)
|
|
258
|
+
|
|
259
|
+
def clear_inactive_default_captions!(properties, types)
|
|
260
|
+
properties.each do |key, property|
|
|
261
|
+
next unless key.end_with?('Caption') && property.dig('Value', 'TextTemplate')
|
|
262
|
+
|
|
263
|
+
controller = properties[key.delete_suffix('Caption')]
|
|
264
|
+
next unless controller&.dig('Value', 'PrimitiveValue') == 'false'
|
|
265
|
+
|
|
266
|
+
value_type = types.dig(key, 'ValueType')
|
|
267
|
+
next unless canonical_schema(property['Value']) == canonical_schema(default_value(value_type))
|
|
268
|
+
|
|
269
|
+
replacement = default_value(value_type)
|
|
270
|
+
replacement['$ID'] = property.dig('Value', '$ID')
|
|
271
|
+
replacement['TypePointer'] = value_type['$ID']
|
|
272
|
+
replacement['TextTemplate'] = nil
|
|
273
|
+
property['Value'] = replacement
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def rebind_value(old_value, old_type, new_type, unit_id, path, issues)
|
|
278
|
+
return issue(issues, unit_id, path, :malformed_widget_value, 'property Value is not an object') unless
|
|
279
|
+
old_value.is_a?(Hash)
|
|
280
|
+
|
|
281
|
+
result = normalized_value(old_value, new_type)
|
|
282
|
+
result['TypePointer'] = new_type['$ID']
|
|
283
|
+
return result unless old_type['Type'] == 'Object'
|
|
284
|
+
|
|
285
|
+
objects = array_items(old_value['Objects'])
|
|
286
|
+
if objects.empty?
|
|
287
|
+
result['Objects'] = IO::BsonCodec.build_array([], marker: array_marker(old_value['Objects']))
|
|
288
|
+
return result
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
old_object_type = old_type['ObjectType']
|
|
292
|
+
new_object_type = new_type['ObjectType']
|
|
293
|
+
unless old_object_type.is_a?(Hash) && new_object_type.is_a?(Hash)
|
|
294
|
+
return issue(issues, unit_id, path, :changed_widget_object,
|
|
295
|
+
'nested Object schema is unavailable')
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
objects = objects.map.with_index do |object, index|
|
|
299
|
+
rebound = rebind_object(
|
|
300
|
+
object, old_object_type, object_template(new_object_type), new_object_type,
|
|
301
|
+
unit_id, "#{path}[#{index}]", issues
|
|
302
|
+
)
|
|
303
|
+
return unless rebound
|
|
304
|
+
|
|
305
|
+
rebound
|
|
306
|
+
end
|
|
307
|
+
result['Objects'] = IO::BsonCodec.build_array(objects, marker: array_marker(old_value['Objects']))
|
|
308
|
+
result
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def normalized_value(old_value, value_type)
|
|
312
|
+
result = default_value(value_type)
|
|
313
|
+
fields = ACTIVE_VALUE_FIELDS.fetch(value_type['Type'], %w[PrimitiveValue])
|
|
314
|
+
fields.each do |field|
|
|
315
|
+
result[field] = normalize_source_variable(deep_copy(old_value[field])) if old_value.key?(field)
|
|
316
|
+
end
|
|
317
|
+
preserve_ids!(result, old_value)
|
|
318
|
+
result
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def convert_value(old_value, old_type, new_type, unit_id, path, issues)
|
|
322
|
+
if old_type['Type'] == 'Boolean' && new_type['Type'] == 'Expression'
|
|
323
|
+
primitive = old_value&.fetch('PrimitiveValue', nil)
|
|
324
|
+
if %w[true false].include?(primitive) && boolean_value_only?(old_value, old_type)
|
|
325
|
+
result = default_value(new_type)
|
|
326
|
+
result['Expression'] = primitive
|
|
327
|
+
result['TypePointer'] = new_type['$ID']
|
|
328
|
+
preserve_ids!(result, old_value)
|
|
329
|
+
return result
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
issue(
|
|
334
|
+
issues, unit_id, path, :changed_widget_property,
|
|
335
|
+
"property changed from #{old_type['Type']} to #{new_type['Type']} without a lossless conversion"
|
|
336
|
+
)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def boolean_value_only?(value, value_type)
|
|
340
|
+
expected = default_value(value_type)
|
|
341
|
+
actual = deep_copy(value)
|
|
342
|
+
actual['PrimitiveValue'] = expected['PrimitiveValue']
|
|
343
|
+
canonical_schema(actual) == canonical_schema(expected)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def object_template(object_type)
|
|
347
|
+
properties = property_types(object_type).reject { _1.dig('ValueType', 'Type') == 'System' }.map do |type|
|
|
348
|
+
value_type = type.fetch('ValueType')
|
|
349
|
+
property = {
|
|
350
|
+
type: value_type.fetch('Type'), default: value_type.fetch('DefaultValue', '').to_s,
|
|
351
|
+
translations: [], required: value_type.fetch('Required', false),
|
|
352
|
+
selection_types: array_items(value_type['SelectionTypes'])
|
|
353
|
+
}
|
|
354
|
+
{
|
|
355
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetProperty',
|
|
356
|
+
'TypePointer' => type['$ID'],
|
|
357
|
+
'Value' => WidgetPackage.allocate.send(:widget_value, value_type, property)
|
|
358
|
+
}
|
|
359
|
+
end
|
|
360
|
+
{
|
|
361
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetObject',
|
|
362
|
+
'Properties' => IO::BsonCodec.build_array(properties, marker: 2),
|
|
363
|
+
'TypePointer' => object_type['$ID']
|
|
364
|
+
}
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def default_widget_value?(value, value_type)
|
|
368
|
+
canonical_schema(value) == canonical_schema(default_value(value_type))
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def default_value(value_type)
|
|
372
|
+
translations = array_items(value_type['Translations']).map do |translation|
|
|
373
|
+
[translation['LanguageCode'].to_s, translation['Text'].to_s]
|
|
374
|
+
end
|
|
375
|
+
property = {
|
|
376
|
+
type: value_type.fetch('Type'), default: value_type.fetch('DefaultValue', '').to_s,
|
|
377
|
+
translations:, required: value_type.fetch('Required', false),
|
|
378
|
+
selection_types: array_items(value_type['SelectionTypes'])
|
|
379
|
+
}
|
|
380
|
+
WidgetPackage.allocate.send(:widget_value, value_type, property)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def migrate_layout_row!(row, unit_id, path, issues, counts)
|
|
384
|
+
columns = array_items(row['Columns'])
|
|
385
|
+
return if columns.empty?
|
|
386
|
+
|
|
387
|
+
replacements = {}
|
|
388
|
+
WEIGHT_KEYS.each do |key|
|
|
389
|
+
values = columns.map { _1[key] }
|
|
390
|
+
next if values.all? { _1.is_a?(Integer) && _1.positive? } && values.sum == 12
|
|
391
|
+
|
|
392
|
+
normalized = normalize_weights(values)
|
|
393
|
+
unless normalized
|
|
394
|
+
return issue(issues, unit_id, path, :unsafe_layout_weights,
|
|
395
|
+
"#{key} values cannot be normalized safely: #{values.inspect}")
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
replacements[key] = normalized
|
|
399
|
+
end
|
|
400
|
+
return if replacements.empty?
|
|
401
|
+
|
|
402
|
+
replacements.each do |key, values|
|
|
403
|
+
columns.each_with_index { |column, index| column[key] = values.fetch(index) }
|
|
404
|
+
end
|
|
405
|
+
counts[:layout_rows] += 1
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def migrate_design_properties!(owner, unit_id, path, issues, counts)
|
|
409
|
+
raw = owner['DesignProperties']
|
|
410
|
+
items = array_items(raw)
|
|
411
|
+
mapping = spacing_mappings
|
|
412
|
+
return if items.empty?
|
|
413
|
+
|
|
414
|
+
changed = migrate_simple_design_aliases!(items)
|
|
415
|
+
retained = []
|
|
416
|
+
compounds = items.select { compound_design_property?(_1) }.to_h { [_1['Key'], _1] }
|
|
417
|
+
items.each do |item|
|
|
418
|
+
old_name = design_property_name(item)
|
|
419
|
+
target = mapping[old_name]
|
|
420
|
+
unless target
|
|
421
|
+
retained << item
|
|
422
|
+
next
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
compound = compounds[target.fetch(:key)] ||= compound_design_property(target.fetch(:key))
|
|
426
|
+
properties = array_items(compound.dig('Value', 'Properties'))
|
|
427
|
+
existing = properties.find { _1['Key'] == target.fetch(:child) }
|
|
428
|
+
if existing && existing.dig('Value', 'Option') != target.fetch(:option)
|
|
429
|
+
issue(issues, unit_id, path, :conflicting_design_property,
|
|
430
|
+
"#{old_name.inspect} conflicts with #{target.fetch(:child).inspect}")
|
|
431
|
+
retained << item
|
|
432
|
+
next
|
|
433
|
+
end
|
|
434
|
+
unless existing
|
|
435
|
+
properties << option_design_property(target.fetch(:child), target.fetch(:option))
|
|
436
|
+
compound['Value']['Properties'] = IO::BsonCodec.build_array(properties, marker: 2)
|
|
437
|
+
end
|
|
438
|
+
retained << compound unless retained.include?(compound)
|
|
439
|
+
changed += 1
|
|
440
|
+
end
|
|
441
|
+
return if changed.zero?
|
|
442
|
+
|
|
443
|
+
owner['DesignProperties'] = IO::BsonCodec.build_array(retained, marker: array_marker(raw))
|
|
444
|
+
counts[:design_properties] += changed
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def spacing_mappings
|
|
448
|
+
return @spacing_mappings if defined?(@spacing_mappings)
|
|
449
|
+
|
|
450
|
+
mappings = {}
|
|
451
|
+
Dir.glob(File.join(@root, 'themesource', '*', '{web,native}', 'design-properties.json')).sort.each do |path|
|
|
452
|
+
document = JSON.parse(File.read(path))
|
|
453
|
+
document.each_value do |properties|
|
|
454
|
+
Array(properties).each { collect_spacing_mappings(_1, mappings) }
|
|
455
|
+
end
|
|
456
|
+
rescue JSON::ParserError
|
|
457
|
+
next
|
|
458
|
+
end
|
|
459
|
+
@spacing_mappings = mappings
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def migrate_simple_design_aliases!(items)
|
|
463
|
+
mappings = design_alias_mappings
|
|
464
|
+
items.count do |item|
|
|
465
|
+
option = item.dig('Value', 'Option')
|
|
466
|
+
target = mappings[[item['Key'], option]]
|
|
467
|
+
next false unless target
|
|
468
|
+
|
|
469
|
+
item['Key'] = target.fetch(:key)
|
|
470
|
+
item['Value']['Option'] = target.fetch(:option) if target.key?(:option)
|
|
471
|
+
true
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def design_alias_mappings
|
|
476
|
+
return @design_alias_mappings if defined?(@design_alias_mappings)
|
|
477
|
+
|
|
478
|
+
mappings = {}
|
|
479
|
+
design_property_documents.each do |document|
|
|
480
|
+
document.each_value do |properties|
|
|
481
|
+
Array(properties).each { collect_design_aliases(_1, mappings) }
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
@design_alias_mappings = mappings.reject { |_key, value| value == :conflict }
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def design_property_documents
|
|
488
|
+
paths = Dir.glob(File.join(@root, 'themesource', '*', '{web,native}', 'design-properties.json')).sort
|
|
489
|
+
paths.filter_map do |path|
|
|
490
|
+
JSON.parse(File.read(path))
|
|
491
|
+
rescue JSON::ParserError
|
|
492
|
+
nil
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def collect_design_aliases(property, mappings)
|
|
497
|
+
return unless property.is_a?(Hash) && property['name'] && property['type'] != 'Spacing'
|
|
498
|
+
|
|
499
|
+
new_key = property.fetch('name')
|
|
500
|
+
old_keys = Array(property['oldNames'])
|
|
501
|
+
options = Array(property['options'])
|
|
502
|
+
old_keys.each { add_design_alias(mappings, [_1, nil], key: new_key) }
|
|
503
|
+
options.each do |option|
|
|
504
|
+
new_option = option['name']
|
|
505
|
+
old_options = Array(option['oldNames'])
|
|
506
|
+
old_options.each { add_design_alias(mappings, [new_key, _1], key: new_key, option: new_option) }
|
|
507
|
+
old_keys.each { add_design_alias(mappings, [_1, new_option], key: new_key, option: new_option) }
|
|
508
|
+
old_keys.product(old_options).each do |old_key, old_option|
|
|
509
|
+
add_design_alias(mappings, [old_key, old_option], key: new_key, option: new_option)
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def add_design_alias(mappings, source, target)
|
|
515
|
+
mappings[source] = :conflict if mappings.key?(source) && mappings[source] != target
|
|
516
|
+
mappings[source] ||= target.freeze
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def collect_spacing_mappings(property, mappings)
|
|
520
|
+
return unless property.is_a?(Hash) && property['type'] == 'Spacing'
|
|
521
|
+
|
|
522
|
+
%w[margin padding].each do |mode|
|
|
523
|
+
Array(property[mode]).each do |size|
|
|
524
|
+
%w[top right bottom left].each do |direction|
|
|
525
|
+
Array(size.dig(direction, 'oldNames')).each do |old_name|
|
|
526
|
+
target = {
|
|
527
|
+
key: property.fetch('name'), child: "#{mode}-#{direction}",
|
|
528
|
+
option: size.fetch('name')
|
|
529
|
+
}.freeze
|
|
530
|
+
mappings[old_name] = target if !mappings.key?(old_name) || mappings[old_name] == target
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def design_property_name(item)
|
|
538
|
+
return unless item.is_a?(Hash) && item.dig('Value', '$Type') == 'Forms$OptionDesignPropertyValue'
|
|
539
|
+
|
|
540
|
+
"#{item['Key']}::#{item.dig('Value', 'Option')}"
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def compound_design_property?(item)
|
|
544
|
+
item.is_a?(Hash) && item.dig('Value', '$Type') == 'Forms$CompoundDesignPropertyValue'
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def compound_design_property(key)
|
|
548
|
+
{
|
|
549
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$DesignPropertyValue', 'Key' => key,
|
|
550
|
+
'Value' => {
|
|
551
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$CompoundDesignPropertyValue',
|
|
552
|
+
'Properties' => IO::BsonCodec.build_array([], marker: 2)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def option_design_property(key, option)
|
|
558
|
+
{
|
|
559
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$DesignPropertyValue', 'Key' => key,
|
|
560
|
+
'Value' => {
|
|
561
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$OptionDesignPropertyValue', 'Option' => option
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def normalize_weights(values)
|
|
567
|
+
return unless values.all? { _1.is_a?(Integer) && !_1.zero? }
|
|
568
|
+
|
|
569
|
+
positive_total = values.select(&:positive?).sum
|
|
570
|
+
return if positive_total >= 12 && values.any?(&:negative?)
|
|
571
|
+
return if positive_total > 12
|
|
572
|
+
|
|
573
|
+
negative = values.each_index.select { values[_1].negative? }
|
|
574
|
+
return if negative.empty?
|
|
575
|
+
|
|
576
|
+
available = 12 - positive_total
|
|
577
|
+
weights = negative.map { values[_1].abs }
|
|
578
|
+
allocated = apportion(available, weights)
|
|
579
|
+
return if allocated.any?(&:zero?)
|
|
580
|
+
|
|
581
|
+
values.map.with_index { |value, index| value.positive? ? value : allocated.fetch(negative.index(index)) }
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def apportion(total, weights)
|
|
585
|
+
denominator = weights.sum
|
|
586
|
+
exact = weights.map { Rational(total * _1, denominator) }
|
|
587
|
+
result = exact.map(&:floor)
|
|
588
|
+
remaining = total - result.sum
|
|
589
|
+
order = exact.each_index.sort_by { |index| [-(exact[index] - result[index]), index] }
|
|
590
|
+
order.first(remaining).each { result[_1] += 1 }
|
|
591
|
+
result
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def definition(widget_id)
|
|
595
|
+
return @definitions[widget_id] if @definitions.key?(widget_id)
|
|
596
|
+
|
|
597
|
+
@package_digests ||= {}
|
|
598
|
+
matches = valid_widget_package_paths.filter_map do |package_path|
|
|
599
|
+
found = WidgetPackage.new(package_path).definition(widget_id)
|
|
600
|
+
[package_path, found] if found
|
|
601
|
+
rescue Zip::Error, REXML::ParseException => e
|
|
602
|
+
@invalid_widget_packages ||= {}
|
|
603
|
+
@invalid_widget_packages[package_path] = e
|
|
604
|
+
nil
|
|
605
|
+
end
|
|
606
|
+
if matches.one?
|
|
607
|
+
package_path, found = matches.first
|
|
608
|
+
@package_digests[widget_id] = Digest::SHA256.file(package_path).hexdigest
|
|
609
|
+
return @definitions[widget_id] = found
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
@definition_failures ||= {}
|
|
613
|
+
@definition_failures[widget_id] = if matches.empty?
|
|
614
|
+
[:missing_widget_definition,
|
|
615
|
+
"no installed MPK defines #{widget_id.inspect}"]
|
|
616
|
+
else
|
|
617
|
+
paths = matches.map { relative_package_path(_1.first) }
|
|
618
|
+
[:ambiguous_widget_definition,
|
|
619
|
+
"multiple installed MPKs define #{widget_id.inspect}: " \
|
|
620
|
+
"#{paths.join(', ')}"]
|
|
621
|
+
end
|
|
622
|
+
@definitions[widget_id] = nil
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def definition_failure(widget_id)
|
|
626
|
+
@definition_failures&.fetch(
|
|
627
|
+
widget_id, [:missing_widget_definition, "no installed MPK defines #{widget_id.inspect}"]
|
|
628
|
+
)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def validate_widget_packages!(issues)
|
|
632
|
+
@invalid_widget_packages = {}
|
|
633
|
+
widget_package_paths.each do |package_path|
|
|
634
|
+
WidgetPackage.new(package_path).definition('__mxrb_package_validation__')
|
|
635
|
+
rescue Zip::Error, REXML::ParseException => e
|
|
636
|
+
@invalid_widget_packages[package_path] = e
|
|
637
|
+
issue(
|
|
638
|
+
issues, '', ".widgets.#{File.basename(package_path)}", :invalid_widget_package,
|
|
639
|
+
"cannot read #{relative_package_path(package_path)}: #{e.class}: #{e.message}"
|
|
640
|
+
)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def widget_package_paths
|
|
645
|
+
@widget_package_paths ||= Dir.glob(File.join(@root, 'widgets', '*.mpk')).sort
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def valid_widget_package_paths
|
|
649
|
+
invalid = @invalid_widget_packages || {}
|
|
650
|
+
widget_package_paths.reject { invalid.key?(_1) }
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def relative_package_path(path)
|
|
654
|
+
Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def package_digest(widget_id)
|
|
658
|
+
@package_digests&.fetch(widget_id, nil)
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def apply_audited_package_migration!(object, type, digest)
|
|
662
|
+
return unless digest == AUDITED_DATA_GRID_PACKAGE_SHA256
|
|
663
|
+
|
|
664
|
+
types = property_types(type['ObjectType']).to_h { [_1['PropertyKey'], _1] }
|
|
665
|
+
properties = property_values(object).each_with_object({}) do |property, result|
|
|
666
|
+
property_type = types.values.find { id(_1['$ID']) == id(property['TypePointer']) }
|
|
667
|
+
result[property_type['PropertyKey']] = property if property_type
|
|
668
|
+
end
|
|
669
|
+
pagination = properties.dig('pagination', 'Value', 'PrimitiveValue')
|
|
670
|
+
selection = properties.dig('itemSelection', 'Value', 'Selection')
|
|
671
|
+
clear_text_template!(properties['loadMoreButtonCaption']) unless pagination == 'loadMore'
|
|
672
|
+
clear_text_template!(properties['clearSelectionButtonLabel']) unless selection == 'Multi'
|
|
673
|
+
clear_text_template!(properties['singleSelectionColumnLabel']) unless selection == 'Single'
|
|
674
|
+
clear_audited_data_grid_column_texts!(properties['columns'], types['columns'])
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def clear_text_template!(property)
|
|
678
|
+
property['Value']['TextTemplate'] = nil if property
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def clear_audited_data_grid_column_texts!(columns_property, columns_type)
|
|
682
|
+
return unless columns_property && columns_type
|
|
683
|
+
|
|
684
|
+
object_type = columns_type.dig('ValueType', 'ObjectType')
|
|
685
|
+
types = property_types(object_type).to_h { [id(_1['$ID']), _1] }
|
|
686
|
+
array_items(columns_property.dig('Value', 'Objects')).each do |column|
|
|
687
|
+
values = property_values(column).each_with_object({}) do |property, result|
|
|
688
|
+
type = types[id(property['TypePointer'])]
|
|
689
|
+
result[type['PropertyKey']] = property if type
|
|
690
|
+
end
|
|
691
|
+
content = values.dig('showContentAs', 'Value', 'PrimitiveValue')
|
|
692
|
+
clear_text_template!(values['dynamicText']) unless content == 'dynamicText'
|
|
693
|
+
clear_text_template!(values['exportValue']) unless content == 'customContent'
|
|
694
|
+
clear_text_template!(values['tooltip']) if content == 'customContent'
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def property_types(type) = array_items(type&.fetch('PropertyTypes', nil))
|
|
699
|
+
def property_values(object) = array_items(object&.fetch('Properties', nil))
|
|
700
|
+
def array_items(value) = IO::BsonCodec.parse_array(value)[:items]
|
|
701
|
+
def array_marker(value) = IO::BsonCodec.parse_array(value)[:marker]
|
|
702
|
+
def id(value) = IO::BsonCodec.extract_id(value)
|
|
703
|
+
def custom_widget?(value) = value['$Type'] == 'CustomWidgets$CustomWidget'
|
|
704
|
+
def layout_row?(value) = value['$Type'] == 'Forms$LayoutGridRow'
|
|
705
|
+
|
|
706
|
+
def canonical_schema(value)
|
|
707
|
+
case value
|
|
708
|
+
when Hash
|
|
709
|
+
value.each_with_object({}) do |(key, child), result|
|
|
710
|
+
next if ['$ID', 'TypePointer'].include?(key)
|
|
711
|
+
|
|
712
|
+
result[key] = canonical_schema(child)
|
|
713
|
+
end
|
|
714
|
+
when Array
|
|
715
|
+
value.map { canonical_schema(_1) }
|
|
716
|
+
else value
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def issue(issues, unit_id, path, kind, message)
|
|
721
|
+
issues << MigrationIssue.new(unit_id, path, kind, message)
|
|
722
|
+
false
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def deep_copy(value)
|
|
726
|
+
case value
|
|
727
|
+
when Hash then value.to_h { |key, child| [key, deep_copy(child)] }
|
|
728
|
+
when Array then value.map { deep_copy(_1) }
|
|
729
|
+
else value
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def preserve_ids!(target, source)
|
|
734
|
+
case target
|
|
735
|
+
when Hash
|
|
736
|
+
return target unless source.is_a?(Hash)
|
|
737
|
+
|
|
738
|
+
target['$ID'] = source['$ID'] if target.key?('$ID') && source.key?('$ID')
|
|
739
|
+
target.each do |key, child|
|
|
740
|
+
preserve_ids!(child, source[key]) if source.key?(key) && key != 'TypePointer'
|
|
741
|
+
end
|
|
742
|
+
when Array
|
|
743
|
+
return target unless source.is_a?(Array)
|
|
744
|
+
|
|
745
|
+
target.zip(source).each { |child, old_child| preserve_ids!(child, old_child) }
|
|
746
|
+
end
|
|
747
|
+
target
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def normalize_source_variable(value)
|
|
751
|
+
case value
|
|
752
|
+
when Hash
|
|
753
|
+
value.each_with_object({}) do |(key, child), result|
|
|
754
|
+
result[key] = normalize_source_variable(child)
|
|
755
|
+
if value['$Type'] == 'Forms$PageVariable' && !value.key?('SubKey') && key == 'SnippetParameter'
|
|
756
|
+
result['SubKey'] = ''
|
|
757
|
+
elsif value['$Type'] == 'Forms$CallNanoflowClientAction' && !value.key?('OutputMappings') &&
|
|
758
|
+
key == 'Nanoflow'
|
|
759
|
+
result['OutputMappings'] = IO::BsonCodec.build_array([], marker: 3)
|
|
760
|
+
elsif value['$Type'] == 'Forms$MicroflowSettings' && !value.key?('OutputMappings') &&
|
|
761
|
+
key == 'Microflow'
|
|
762
|
+
result['OutputMappings'] = IO::BsonCodec.build_array([], marker: 3)
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
when Array then value.map { normalize_source_variable(_1) }
|
|
766
|
+
else value
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
# rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength
|
|
771
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
772
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
773
|
+
# rubocop:enable Lint/NonLocalExitFromIterator
|
|
774
|
+
end
|
|
775
|
+
end
|