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