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,1716 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "digest"
|
|
7
|
+
|
|
8
|
+
module Mxrb
|
|
9
|
+
# Exports an MPR into an editable, layered Ruby source tree.
|
|
10
|
+
class Exporter
|
|
11
|
+
LAYERS = %w[domain application presentation infrastructure].freeze
|
|
12
|
+
MARKETPLACE_PROVENANCE = [
|
|
13
|
+
".mxrb/marketplace.lock.json",
|
|
14
|
+
".mxrb/marketplace",
|
|
15
|
+
".mxrb/marketplace-originals"
|
|
16
|
+
].freeze
|
|
17
|
+
EDITABLE_FLOW_OBJECT_TYPES = %w[
|
|
18
|
+
Microflows$StartEvent
|
|
19
|
+
Microflows$EndEvent
|
|
20
|
+
Microflows$ActionActivity
|
|
21
|
+
Microflows$ExclusiveSplit
|
|
22
|
+
Microflows$InheritanceSplit
|
|
23
|
+
Microflows$ExclusiveMerge
|
|
24
|
+
Microflows$LoopedActivity
|
|
25
|
+
Microflows$ErrorEvent
|
|
26
|
+
Microflows$ContinueEvent
|
|
27
|
+
Microflows$Annotation
|
|
28
|
+
Microflows$MicroflowParameter
|
|
29
|
+
].freeze
|
|
30
|
+
EDITABLE_ACTION_TYPES = %w[
|
|
31
|
+
Microflows$CreateObjectAction
|
|
32
|
+
Microflows$CreateChangeAction
|
|
33
|
+
Microflows$ChangeObjectAction
|
|
34
|
+
Microflows$ChangeAction
|
|
35
|
+
Microflows$RetrieveAction
|
|
36
|
+
Microflows$CommitObjectsAction
|
|
37
|
+
Microflows$CommitAction
|
|
38
|
+
Microflows$DeleteAction
|
|
39
|
+
Microflows$MicroflowCallAction
|
|
40
|
+
Microflows$CreateVariableAction
|
|
41
|
+
Microflows$ChangeVariableAction
|
|
42
|
+
Microflows$ShowMessageAction
|
|
43
|
+
Microflows$LogMessageAction
|
|
44
|
+
Microflows$ShowFormAction
|
|
45
|
+
Microflows$CloseFormAction
|
|
46
|
+
Microflows$JavaActionCallAction
|
|
47
|
+
Microflows$JavaScriptActionCallAction
|
|
48
|
+
Microflows$NanoflowCallAction
|
|
49
|
+
Microflows$AppServiceCallAction
|
|
50
|
+
Microflows$AggregateAction
|
|
51
|
+
Microflows$RollbackAction
|
|
52
|
+
Microflows$CastAction
|
|
53
|
+
Microflows$CreateListAction
|
|
54
|
+
Microflows$ListOperationsAction
|
|
55
|
+
Microflows$ChangeListAction
|
|
56
|
+
Microflows$ValidationFeedbackAction
|
|
57
|
+
Microflows$RestCallAction
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
def initialize(mpr_path, output_dir)
|
|
61
|
+
@mpr_path = File.expand_path(mpr_path)
|
|
62
|
+
@output_dir = File.expand_path(output_dir)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def export!(parallel: true)
|
|
66
|
+
FileUtils.mkdir_p(@output_dir)
|
|
67
|
+
Mxrb.open(@mpr_path) do |project|
|
|
68
|
+
@architecture = project.architecture_definition
|
|
69
|
+
export_app_structure
|
|
70
|
+
export_project_assets
|
|
71
|
+
export_native_units(project)
|
|
72
|
+
export_security(project)
|
|
73
|
+
export_architecture_contracts(project)
|
|
74
|
+
mods = project.modules
|
|
75
|
+
if parallel && mods.size > 1
|
|
76
|
+
threads = mods.map { |mod| Thread.new { export_module(mod) } }
|
|
77
|
+
threads.each(&:join)
|
|
78
|
+
else
|
|
79
|
+
mods.each { export_module(_1) }
|
|
80
|
+
end
|
|
81
|
+
write_project(project)
|
|
82
|
+
end
|
|
83
|
+
@output_dir
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def export_module(mod)
|
|
89
|
+
root = File.join(@output_dir, "modules", mod.name)
|
|
90
|
+
LAYERS.each { FileUtils.mkdir_p(File.join(root, _1)) }
|
|
91
|
+
|
|
92
|
+
export_domain(root, mod)
|
|
93
|
+
export_microflows(root, mod)
|
|
94
|
+
export_pages(root, mod)
|
|
95
|
+
export_menus(root, mod)
|
|
96
|
+
export_nanoflows(root, mod)
|
|
97
|
+
export_module_scaffolding(root)
|
|
98
|
+
export_module_security(root, mod)
|
|
99
|
+
write(File.join(root, "module.rb"), module_source(mod))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def export_app_structure
|
|
103
|
+
%w[
|
|
104
|
+
app/security app/navigation app/design_system
|
|
105
|
+
theme resources themesource widgets javasource javascriptsource
|
|
106
|
+
].each { write(File.join(@output_dir, _1, ".keep"), "") }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def export_project_assets
|
|
110
|
+
source_root = File.dirname(@mpr_path)
|
|
111
|
+
files = (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory|
|
|
112
|
+
root = File.join(source_root, directory)
|
|
113
|
+
next [root] if File.file?(root) && !File.symlink?(root)
|
|
114
|
+
next [] unless File.directory?(root) && !File.symlink?(root)
|
|
115
|
+
|
|
116
|
+
Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH)
|
|
117
|
+
end.select { File.file?(_1) && !File.symlink?(_1) }.sort
|
|
118
|
+
entries = files.map do |source|
|
|
119
|
+
relative = Pathname.new(source).relative_path_from(Pathname.new(source_root)).to_s
|
|
120
|
+
target = File.join(@output_dir, relative)
|
|
121
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
122
|
+
FileUtils.cp(source, target)
|
|
123
|
+
{
|
|
124
|
+
"path" => relative,
|
|
125
|
+
"size" => File.size(source),
|
|
126
|
+
"sha256" => Digest::SHA256.file(source).hexdigest
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
write(
|
|
130
|
+
File.join(@output_dir, ".mxrb", "assets.json"),
|
|
131
|
+
JSON.pretty_generate("version" => 1, "files" => entries)
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def export_native_units(project)
|
|
136
|
+
units = project.all_units.filter_map do |unit|
|
|
137
|
+
doc = project.parse_bson(unit)
|
|
138
|
+
type = doc["$Type"]
|
|
139
|
+
next if type.to_s.empty? || type == "Projects$Project"
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
"unit_id" => unit["UnitID"],
|
|
143
|
+
"container_id" => unit["ContainerID"],
|
|
144
|
+
"module" => module_name_for(project, unit),
|
|
145
|
+
"containment" => unit["ContainmentName"],
|
|
146
|
+
"name" => doc["Name"] || doc["name"] || "",
|
|
147
|
+
"type" => type,
|
|
148
|
+
"contents" => Base64.strict_encode64(IO::BsonCodec.serialize(doc))
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
write(
|
|
152
|
+
File.join(@output_dir, ".mxrb", "native_units.json"),
|
|
153
|
+
JSON.pretty_generate("format_version" => project.format_version.to_s, "units" => units)
|
|
154
|
+
)
|
|
155
|
+
source = project.all_units.filter_map do |unit|
|
|
156
|
+
doc = project.parse_bson(unit)
|
|
157
|
+
next if doc["$Type"].to_s.empty? || doc["$Type"] == "Projects$Project"
|
|
158
|
+
|
|
159
|
+
native_unit_source(project, unit, doc)
|
|
160
|
+
end
|
|
161
|
+
write(
|
|
162
|
+
File.join(@output_dir, ".mxrb", "native_units.rb"),
|
|
163
|
+
"# frozen_string_literal: true\n\n#{source.join("\n")}"
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def native_unit_source(project, unit, doc)
|
|
168
|
+
<<~RUBY
|
|
169
|
+
native_unit #{ruby(unit.fetch("UnitID"))},
|
|
170
|
+
container_id: #{ruby(unit.fetch("ContainerID"))},
|
|
171
|
+
containment: #{ruby(unit.fetch("ContainmentName"))},
|
|
172
|
+
module_name: #{ruby(module_name_for(project, unit))},
|
|
173
|
+
deep_structure: #{native_ruby(doc, 20)}
|
|
174
|
+
RUBY
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def module_name_for(project, unit)
|
|
178
|
+
container = unit["ContainerID"]
|
|
179
|
+
project.modules.find { _1.id == container }&.name ||
|
|
180
|
+
project.modules.find { |mod| native_descendant_of?(project, container, mod.id) }&.name
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def native_descendant_of?(project, child_id, ancestor_id)
|
|
184
|
+
current = project.raw_unit(child_id)
|
|
185
|
+
until current.nil? || current["UnitID"] == current["ContainerID"]
|
|
186
|
+
return true if current["ContainerID"] == ancestor_id
|
|
187
|
+
current = project.raw_unit(current["ContainerID"])
|
|
188
|
+
end
|
|
189
|
+
false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def export_security(project)
|
|
193
|
+
doc = project_security_doc(project)
|
|
194
|
+
source = doc ? security_source(doc) : "# frozen_string_literal: true\n"
|
|
195
|
+
write(File.join(@output_dir, "app", "security", "security.rb"), source)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def export_architecture_contracts(project)
|
|
199
|
+
navigation = @architecture&.fetch(:navigation, nil)
|
|
200
|
+
navigation ||= project.navigation.to_h unless project.navigation.empty?
|
|
201
|
+
design_system = @architecture&.fetch(:design_system, nil)
|
|
202
|
+
write(
|
|
203
|
+
File.join(@output_dir, "app", "navigation", "navigation.rb"),
|
|
204
|
+
navigation_source(navigation)
|
|
205
|
+
)
|
|
206
|
+
write(
|
|
207
|
+
File.join(@output_dir, "app", "design_system", "design_system.rb"),
|
|
208
|
+
design_system_source(design_system)
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def export_module_scaffolding(root)
|
|
213
|
+
%w[
|
|
214
|
+
domain/enumerations domain/rules domain/policies
|
|
215
|
+
application/ports/repositories application/queries application/validations application/jobs
|
|
216
|
+
presentation/features presentation/client_actions presentation/snippets presentation/view_models
|
|
217
|
+
presentation/menus
|
|
218
|
+
infrastructure/persistence/mendix infrastructure/persistence/external
|
|
219
|
+
infrastructure/mappings infrastructure/actions infrastructure/endpoints
|
|
220
|
+
infrastructure/integrations
|
|
221
|
+
security
|
|
222
|
+
].each { write(File.join(root, _1, ".keep"), "") }
|
|
223
|
+
|
|
224
|
+
security_path = File.join(root, "security", "security.rb")
|
|
225
|
+
write(security_path, "# frozen_string_literal: true\n") unless File.exist?(security_path)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def export_module_security(root, mod)
|
|
229
|
+
roles = mod.module_roles.map do |role|
|
|
230
|
+
args = [symbol(role.fetch(:name))]
|
|
231
|
+
description = role.fetch(:description, "")
|
|
232
|
+
args << "description: #{ruby(description)}" unless description.empty?
|
|
233
|
+
"module_role #{args.join(', ')}"
|
|
234
|
+
end
|
|
235
|
+
source = (["# frozen_string_literal: true", ""] + roles).join("\n") + "\n"
|
|
236
|
+
write(File.join(root, "security", "security.rb"), source)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def export_domain(root, mod)
|
|
240
|
+
domain = File.join(root, "domain")
|
|
241
|
+
entities_dir = File.join(domain, "entities")
|
|
242
|
+
FileUtils.mkdir_p(entities_dir)
|
|
243
|
+
|
|
244
|
+
associations = mod.associations.group_by(&:from_entity_id)
|
|
245
|
+
architecture = architecture_module(mod.name)
|
|
246
|
+
entity_files = unique_entity_filenames(mod.entities)
|
|
247
|
+
mod.entities.each do |entity|
|
|
248
|
+
entity_metadata = architecture&.fetch(:entities, [])&.find { _1[:name] == entity.name }
|
|
249
|
+
write(
|
|
250
|
+
File.join(entities_dir, entity_files.fetch(entity.id)),
|
|
251
|
+
entity_source(entity, mod, associations.fetch(entity.id, []), entity_metadata)
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
loads = entity_files.values.sort.map do |filename|
|
|
256
|
+
%(evaluate File.join(__dir__, "entities", #{ruby(filename)}))
|
|
257
|
+
end
|
|
258
|
+
write(File.join(domain, "model.rb"), "#{loads.join("\n")}\n")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def export_microflows(root, mod)
|
|
262
|
+
files = unique_filenames(mod.microflows)
|
|
263
|
+
by_layer = { "application" => [], "infrastructure" => [] }
|
|
264
|
+
architecture = architecture_module(mod.name)
|
|
265
|
+
mod.microflows.each do |flow|
|
|
266
|
+
layer, category = flow_location(flow.name)
|
|
267
|
+
relative = File.join(category, files.fetch(flow.id))
|
|
268
|
+
metadata = architecture&.fetch(:microflows, [])&.find { _1[:name] == flow.name }
|
|
269
|
+
write(File.join(root, layer, relative), microflow_source(flow, metadata))
|
|
270
|
+
by_layer.fetch(layer) << relative
|
|
271
|
+
end
|
|
272
|
+
repositories = architecture&.fetch(:repositories, []) || []
|
|
273
|
+
unless repositories.empty?
|
|
274
|
+
relative = File.join("ports", "repositories.rb")
|
|
275
|
+
write(File.join(root, "application", relative), repositories_source(repositories))
|
|
276
|
+
by_layer["application"] << relative
|
|
277
|
+
end
|
|
278
|
+
by_layer.each do |layer, paths|
|
|
279
|
+
write_path_aggregator(File.join(root, layer, "#{layer}.rb"), paths)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def export_pages(root, mod)
|
|
284
|
+
presentation = File.join(root, "presentation")
|
|
285
|
+
documents = File.join(presentation, "pages")
|
|
286
|
+
FileUtils.mkdir_p(documents)
|
|
287
|
+
files = unique_filenames(mod.pages)
|
|
288
|
+
architecture = architecture_module(mod.name)
|
|
289
|
+
paths = []
|
|
290
|
+
mod.pages.each do |page|
|
|
291
|
+
relative = File.join("pages", files.fetch(page.id))
|
|
292
|
+
metadata = architecture&.fetch(:pages, [])&.find { _1[:name] == page.name }
|
|
293
|
+
write(File.join(presentation, relative), page_source(page, metadata))
|
|
294
|
+
paths << relative
|
|
295
|
+
end
|
|
296
|
+
write_path_aggregator(File.join(presentation, "presentation.rb"), paths)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def export_menus(root, mod)
|
|
300
|
+
menus = mod.menus
|
|
301
|
+
return if menus.empty?
|
|
302
|
+
|
|
303
|
+
presentation = File.join(root, "presentation")
|
|
304
|
+
files = unique_filenames(menus)
|
|
305
|
+
paths = []
|
|
306
|
+
menus.each do |menu|
|
|
307
|
+
relative = File.join("menus", files.fetch(menu.id))
|
|
308
|
+
write(File.join(presentation, relative), menu_source(menu))
|
|
309
|
+
paths << relative
|
|
310
|
+
end
|
|
311
|
+
existing_path = File.join(presentation, "presentation.rb")
|
|
312
|
+
existing = File.exist?(existing_path) ? File.read(existing_path).lines : []
|
|
313
|
+
additions = paths.sort.map do |relative|
|
|
314
|
+
segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
|
|
315
|
+
"evaluate File.join(__dir__, #{segments})\n"
|
|
316
|
+
end
|
|
317
|
+
write(existing_path, (existing + additions).join)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def export_nanoflows(root, mod)
|
|
321
|
+
flows = mod.nanoflows
|
|
322
|
+
return if flows.empty?
|
|
323
|
+
presentation = File.join(root, "presentation")
|
|
324
|
+
files = unique_filenames(flows)
|
|
325
|
+
paths = []
|
|
326
|
+
architecture = architecture_module(mod.name)
|
|
327
|
+
flows.each do |flow|
|
|
328
|
+
relative = File.join("client_actions", files.fetch(flow.id))
|
|
329
|
+
metadata = architecture&.fetch(:nanoflows, [])&.find { _1[:name] == flow.name }
|
|
330
|
+
write(File.join(presentation, relative), nanoflow_source(flow, metadata))
|
|
331
|
+
paths << relative
|
|
332
|
+
end
|
|
333
|
+
existing = File.read(File.join(presentation, "presentation.rb")).lines
|
|
334
|
+
additions = paths.sort.map do |relative|
|
|
335
|
+
segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
|
|
336
|
+
"evaluate File.join(__dir__, #{segments})\n"
|
|
337
|
+
end
|
|
338
|
+
write(File.join(presentation, "presentation.rb"), (existing + additions).join)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def write_path_aggregator(path, relative_paths)
|
|
342
|
+
loads = relative_paths.sort.map do |relative|
|
|
343
|
+
segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
|
|
344
|
+
%(evaluate File.join(__dir__, #{segments}))
|
|
345
|
+
end
|
|
346
|
+
write(path, "#{loads.join("\n")}\n")
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def flow_location(name)
|
|
350
|
+
case name
|
|
351
|
+
when /\A(?:DS|OQL)_/ then %w[application queries]
|
|
352
|
+
when /\AVAL_/ then %w[application validations]
|
|
353
|
+
when /\ASE_/ then %w[application jobs]
|
|
354
|
+
when /\AAPI_/ then %w[infrastructure endpoints]
|
|
355
|
+
when /\AINT_/ then %w[infrastructure integrations]
|
|
356
|
+
else %w[application use_cases]
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def write_project(project)
|
|
361
|
+
module_loads = project.modules.map do |mod|
|
|
362
|
+
%( evaluate File.join(__dir__, "modules", #{ruby(mod.name)}, "module.rb"))
|
|
363
|
+
end.join("\n")
|
|
364
|
+
|
|
365
|
+
source = <<~RUBY
|
|
366
|
+
# frozen_string_literal: true
|
|
367
|
+
|
|
368
|
+
require "mxrb"
|
|
369
|
+
|
|
370
|
+
output = ENV.fetch("MXRB_OUTPUT_PATH", File.join(__dir__, #{ruby(File.basename(@mpr_path))}))
|
|
371
|
+
|
|
372
|
+
Mxrb.define(output) do
|
|
373
|
+
mendix_version #{ruby(project.mendix_version || "10.18.0")}
|
|
374
|
+
native_units File.join(__dir__, ".mxrb", "native_units.json")
|
|
375
|
+
project_assets File.join(__dir__, ".mxrb", "assets.json"), root: __dir__
|
|
376
|
+
evaluate File.join(__dir__, ".mxrb", "native_units.rb")
|
|
377
|
+
evaluate File.join(__dir__, "app", "security", "security.rb")
|
|
378
|
+
evaluate File.join(__dir__, "app", "navigation", "navigation.rb")
|
|
379
|
+
evaluate File.join(__dir__, "app", "design_system", "design_system.rb")
|
|
380
|
+
#{module_loads}
|
|
381
|
+
end
|
|
382
|
+
RUBY
|
|
383
|
+
write(File.join(@output_dir, "project.rb"), source)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def module_source(mod)
|
|
387
|
+
<<~RUBY
|
|
388
|
+
# frozen_string_literal: true
|
|
389
|
+
|
|
390
|
+
self.module #{symbol(mod.name)} do
|
|
391
|
+
evaluate File.join(__dir__, "domain", "model.rb")
|
|
392
|
+
evaluate File.join(__dir__, "application", "application.rb")
|
|
393
|
+
evaluate File.join(__dir__, "presentation", "presentation.rb")
|
|
394
|
+
evaluate File.join(__dir__, "infrastructure", "infrastructure.rb")
|
|
395
|
+
evaluate File.join(__dir__, "security", "security.rb")
|
|
396
|
+
end
|
|
397
|
+
RUBY
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def project_security_doc(project)
|
|
401
|
+
raw = project.all_units.find { |unit| project.parse_bson(unit)["$Type"] == "Security$ProjectSecurity" }
|
|
402
|
+
raw && project.parse_bson(raw)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def security_source(doc)
|
|
406
|
+
level = doc["SecurityLevel"]
|
|
407
|
+
roles = IO::BsonCodec.parse_array(doc["UserRoles"]).fetch(:items).map do |role|
|
|
408
|
+
module_roles = IO::BsonCodec.parse_array(role["ModuleRoles"]).fetch(:items)
|
|
409
|
+
admin = role["ManageAllRoles"] == true
|
|
410
|
+
args = [symbol(role.fetch("Name"))]
|
|
411
|
+
args << "module_roles: #{ruby(module_roles)}" unless module_roles.empty?
|
|
412
|
+
args << "admin: true" if admin
|
|
413
|
+
" user_role #{args.join(', ')}"
|
|
414
|
+
end
|
|
415
|
+
options = []
|
|
416
|
+
options << " admin_user_role #{ruby(doc["AdminUserRole"])}" unless doc["AdminUserRole"].to_s.empty?
|
|
417
|
+
options << " demo_users #{doc["EnableDemoUsers"] == true}"
|
|
418
|
+
guest = " guest_access #{doc["EnableGuestAccess"] == true}"
|
|
419
|
+
guest += ", role: #{ruby(doc["GuestUserRole"])}" unless doc["GuestUserRole"].to_s.empty?
|
|
420
|
+
options << guest
|
|
421
|
+
options << " sign_in_microflow #{ruby(doc["SignInMicroflow"])}" unless doc["SignInMicroflow"].to_s.empty?
|
|
422
|
+
password = doc["PasswordPolicySettings"]
|
|
423
|
+
if password.is_a?(Hash)
|
|
424
|
+
known = {
|
|
425
|
+
"MinimumLength" => :minimum_length,
|
|
426
|
+
"RequireDigit" => :require_digit,
|
|
427
|
+
"RequireMixedCase" => :require_mixed_case,
|
|
428
|
+
"RequireSymbol" => :require_symbol
|
|
429
|
+
}
|
|
430
|
+
policy = password.each_with_object({}) do |(key, value), result|
|
|
431
|
+
next if %w[$ID $Type].include?(key)
|
|
432
|
+
|
|
433
|
+
result[known.fetch(key, key.to_sym)] = value
|
|
434
|
+
end
|
|
435
|
+
options << " password_policy(**#{ruby(policy)})"
|
|
436
|
+
end
|
|
437
|
+
<<~RUBY
|
|
438
|
+
# frozen_string_literal: true
|
|
439
|
+
|
|
440
|
+
security do
|
|
441
|
+
security_level #{ruby(level)}
|
|
442
|
+
#{roles.join("\n")}
|
|
443
|
+
#{options.join("\n")}
|
|
444
|
+
end
|
|
445
|
+
RUBY
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def navigation_source(navigation)
|
|
449
|
+
return "# frozen_string_literal: true\n" unless navigation
|
|
450
|
+
|
|
451
|
+
profiles = navigation.fetch(:profiles, []).flat_map { navigation_profile_source(_1) }
|
|
452
|
+
<<~RUBY
|
|
453
|
+
# frozen_string_literal: true
|
|
454
|
+
|
|
455
|
+
navigation do
|
|
456
|
+
#{profiles.join("\n")}
|
|
457
|
+
end
|
|
458
|
+
RUBY
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def navigation_profile_source(profile)
|
|
462
|
+
args = [symbol(profile.fetch(:name))]
|
|
463
|
+
args << "home_page: #{ruby(profile[:home_page])}" if profile[:home_page]
|
|
464
|
+
args << "home_microflow: #{ruby(profile[:home_microflow])}" if profile[:home_microflow]
|
|
465
|
+
args << "sign_in_page: #{ruby(profile[:sign_in_page])}" if profile[:sign_in_page]
|
|
466
|
+
args << "menu: #{ruby(profile[:menu])}" if profile[:menu]
|
|
467
|
+
role_homes = profile.fetch(:role_homes, {})
|
|
468
|
+
args << "role_homes: #{ruby(role_homes)}" if role_homes.is_a?(Hash) && !role_homes.empty?
|
|
469
|
+
args << "offline: true" if profile[:offline]
|
|
470
|
+
args << "kind: #{ruby(profile[:kind])}" unless profile[:kind].to_s.empty?
|
|
471
|
+
args << "app_icon: #{ruby(profile[:app_icon])}" if profile[:app_icon]
|
|
472
|
+
body = navigation_profile_body(profile, role_homes)
|
|
473
|
+
return [" profile #{args.join(', ')}"] if body.empty?
|
|
474
|
+
|
|
475
|
+
[" profile #{args.join(', ')} do", *body, " end"]
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def navigation_profile_body(profile, role_homes)
|
|
479
|
+
details = role_homes.is_a?(Array) ? role_homes : profile.fetch(:role_home_details, [])
|
|
480
|
+
titles = profile.fetch(:app_title, {}).map do |locale, text|
|
|
481
|
+
" title #{ruby(locale)}, #{ruby(text)}"
|
|
482
|
+
end
|
|
483
|
+
homes = details.map do |home|
|
|
484
|
+
options = []
|
|
485
|
+
options << "page: #{ruby(home[:page])}" if home[:page]
|
|
486
|
+
options << "microflow: #{ruby(home[:microflow])}" if home[:microflow]
|
|
487
|
+
suffix = options.empty? ? "" : ", #{options.join(', ')}"
|
|
488
|
+
" home_for #{ruby(home.fetch(:role))}#{suffix}"
|
|
489
|
+
end
|
|
490
|
+
items = profile.fetch(:items, []).flat_map { navigation_item_source(_1, 4) }
|
|
491
|
+
titles + homes + items
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def design_system_source(design_system)
|
|
495
|
+
return "# frozen_string_literal: true\n" unless design_system
|
|
496
|
+
|
|
497
|
+
lines = design_system.fetch(:tokens, []).map do |token|
|
|
498
|
+
args = [symbol(token.fetch(:name))]
|
|
499
|
+
args << "value: #{ruby(token[:value])}" unless token[:value].nil?
|
|
500
|
+
" #{token.fetch(:kind)} #{args.join(', ')}"
|
|
501
|
+
end
|
|
502
|
+
lines.concat(design_system.fetch(:layouts, []).map { " layout #{ruby(_1)}" })
|
|
503
|
+
lines.concat(design_system.fetch(:components, []).map { " component #{ruby(_1)}" })
|
|
504
|
+
lines.concat(design_system.fetch(:accessibility, []).map { " accessibility #{ruby(_1)}" })
|
|
505
|
+
design_system.fetch(:themes, []).each do |theme|
|
|
506
|
+
args = [ruby(theme.fetch(:name))]
|
|
507
|
+
args << "inherits: #{ruby(theme[:inherits])}" if theme[:inherits]
|
|
508
|
+
lines << " theme #{args.join(', ')} do"
|
|
509
|
+
theme.fetch(:tokens, []).each do |token|
|
|
510
|
+
token_args = [symbol(token.fetch(:name))]
|
|
511
|
+
token_args << "value: #{ruby(token[:value])}" unless token[:value].nil?
|
|
512
|
+
lines << " #{token.fetch(:kind)} #{token_args.join(', ')}"
|
|
513
|
+
end
|
|
514
|
+
lines << " end"
|
|
515
|
+
end
|
|
516
|
+
design_system.fetch(:contrast_pairs, []).each do |pair|
|
|
517
|
+
lines << " contrast foreground: #{ruby(pair.fetch(:foreground))}, " \
|
|
518
|
+
"background: #{ruby(pair.fetch(:background))}, level: #{symbol(pair.fetch(:level))}"
|
|
519
|
+
end
|
|
520
|
+
lines << " forbid_literal_colors" if design_system[:forbid_literal_colors]
|
|
521
|
+
<<~RUBY
|
|
522
|
+
# frozen_string_literal: true
|
|
523
|
+
|
|
524
|
+
design_system do
|
|
525
|
+
#{lines.join("\n")}
|
|
526
|
+
end
|
|
527
|
+
RUBY
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def navigation_item_source(item, indent)
|
|
531
|
+
pad = " " * indent
|
|
532
|
+
caption = item.fetch(:caption)
|
|
533
|
+
primary = caption["en_US"] || caption[:en_US] || caption.values.first || ""
|
|
534
|
+
translations = caption.reject { |locale, _| locale.to_s == "en_US" }
|
|
535
|
+
args = [ruby(primary)]
|
|
536
|
+
args << "page: #{ruby(item[:page])}" if item[:page]
|
|
537
|
+
args << "microflow: #{ruby(item[:microflow])}" if item[:microflow]
|
|
538
|
+
args << "icon: #{ruby(item[:icon])}" if item[:icon]
|
|
539
|
+
args << "translations: #{ruby(translations)}" unless translations.empty?
|
|
540
|
+
children = item.fetch(:items, [])
|
|
541
|
+
return ["#{pad}item #{args.join(', ')}"] if children.empty?
|
|
542
|
+
|
|
543
|
+
["#{pad}item #{args.join(', ')} do",
|
|
544
|
+
*children.flat_map { navigation_item_source(_1, indent + 2) },
|
|
545
|
+
"#{pad}end"]
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def entity_source(entity, mod, associations, metadata = nil)
|
|
549
|
+
attrs = entity.attributes.map do |attr|
|
|
550
|
+
options = []
|
|
551
|
+
options << "default: #{ruby(attr.default_value)}" unless attr.default_value.nil? || attr.default_value == ""
|
|
552
|
+
options << "documentation: #{ruby(attr.documentation)}" unless attr.documentation.to_s.empty?
|
|
553
|
+
options << "length: #{attr.length}" unless attr.length.nil?
|
|
554
|
+
options << "localize_date: #{attr.localize_date}" unless attr.localize_date.nil?
|
|
555
|
+
options << "enumeration: #{ruby(attr.enumeration)}" unless attr.enumeration.to_s.empty?
|
|
556
|
+
options << 'required: true' if attr.required
|
|
557
|
+
options << 'unique: true' if attr.unique
|
|
558
|
+
" #{attr.type || :string} #{symbol(attr.name)}#{options.empty? ? '' : ", #{options.join(', ')}"}"
|
|
559
|
+
end
|
|
560
|
+
assocs = associations.map do |assoc|
|
|
561
|
+
target = mod.entities.find { _1.id == assoc.to_entity_id }&.name || assoc.to_entity_id
|
|
562
|
+
qualified_target = target.to_s.include?('.') ? target : "#{mod.name}.#{target}"
|
|
563
|
+
cardinality = association_cardinality(assoc)
|
|
564
|
+
options = ["cardinality: #{symbol(cardinality)}"]
|
|
565
|
+
options << "name: #{ruby(assoc.name)}"
|
|
566
|
+
documentation = assoc.respond_to?(:documentation) ? assoc.documentation : nil
|
|
567
|
+
options << "documentation: #{ruby(documentation)}" unless documentation.to_s.empty?
|
|
568
|
+
inferred_storage = assoc.association_type == :ReferenceSet ? :Table : :Column
|
|
569
|
+
storage_format = assoc.respond_to?(:storage_format) ? assoc.storage_format : nil
|
|
570
|
+
if storage_format && storage_format != inferred_storage
|
|
571
|
+
options << "storage_format: #{symbol(storage_format)}"
|
|
572
|
+
end
|
|
573
|
+
parent_delete = assoc.respond_to?(:parent_delete_behavior) ?
|
|
574
|
+
assoc.parent_delete_behavior : :NoAction
|
|
575
|
+
child_delete = assoc.respond_to?(:child_delete_behavior) ?
|
|
576
|
+
assoc.child_delete_behavior : :NoAction
|
|
577
|
+
options << "parent_delete: #{symbol(parent_delete)}" unless parent_delete == :NoAction
|
|
578
|
+
options << "child_delete: #{symbol(child_delete)}" unless child_delete == :NoAction
|
|
579
|
+
" association #{ruby(qualified_target)}, #{options.join(', ')}"
|
|
580
|
+
end
|
|
581
|
+
flags = []
|
|
582
|
+
flags << " non_persistent!" unless entity.persistable
|
|
583
|
+
flags << " documentation #{ruby(entity.documentation)}" unless entity.documentation.to_s.empty?
|
|
584
|
+
generalization = entity.respond_to?(:generalization_target) ? entity.generalization_target : nil
|
|
585
|
+
flags << " generalizes #{ruby(generalization)}" if generalization
|
|
586
|
+
system_members = entity.respond_to?(:system_members) ? entity.system_members : {}
|
|
587
|
+
system_options = system_members.to_h.select { |_key, value| value }
|
|
588
|
+
unless system_options.empty?
|
|
589
|
+
flags << " system_members #{system_options.map { |key, value| "#{key}: #{value}" }.join(', ')}"
|
|
590
|
+
end
|
|
591
|
+
indexes = entity.respond_to?(:indexes) ? entity.indexes : []
|
|
592
|
+
Array(indexes).each do |index|
|
|
593
|
+
indexed = IO::BsonCodec.parse_array(
|
|
594
|
+
index['attributes'] || index['Attributes'] || index['IndexedAttributes']
|
|
595
|
+
)[:items]
|
|
596
|
+
members = indexed.filter_map { _1['attribute'] || _1['Attribute'] }.map { _1.to_s.split('.').last }
|
|
597
|
+
next if members.empty?
|
|
598
|
+
|
|
599
|
+
directions = indexed.map { |member| member.fetch('ascending', member.fetch('Ascending', true)) }
|
|
600
|
+
options = []
|
|
601
|
+
options << "ascending: #{ruby(directions)}" unless directions.all?
|
|
602
|
+
include_offline = index['includeInOffline'] || index['IncludeInOffline']
|
|
603
|
+
options << 'include_offline: true' if include_offline
|
|
604
|
+
flags << " index #{members.map { symbol(_1) }.join(', ')}#{options.empty? ? '' : ", #{options.join(', ')}"}"
|
|
605
|
+
end
|
|
606
|
+
metadata&.fetch(:lifecycle, [])&.each do |callback|
|
|
607
|
+
flags << " #{callback.fetch(:event)} microflow: #{symbol(callback.fetch(:handler))}"
|
|
608
|
+
end
|
|
609
|
+
access_rules = Array(entity.access_rules)
|
|
610
|
+
access_lines = access_rules.filter_map { |rule| access_rule_source(rule) }
|
|
611
|
+
# A partial access-rule export would replace the complete native rule
|
|
612
|
+
# collection. Keep the collection opaque unless every rule is editable.
|
|
613
|
+
access_lines = [] unless access_lines.size == access_rules.size
|
|
614
|
+
<<~RUBY
|
|
615
|
+
# frozen_string_literal: true
|
|
616
|
+
|
|
617
|
+
entity #{symbol(entity.name)} do
|
|
618
|
+
#{(attrs + assocs + flags + access_lines).join("\n")}
|
|
619
|
+
end
|
|
620
|
+
RUBY
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def association_cardinality(association)
|
|
624
|
+
return :many_to_many if association.association_type == :ReferenceSet
|
|
625
|
+
return :one_to_one if association.owner == :Both
|
|
626
|
+
|
|
627
|
+
:many_to_one
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def access_rule_source(rule)
|
|
631
|
+
role_list = rule.fetch(:roles).reject { _1.to_s.empty? }
|
|
632
|
+
return nil if role_list.empty?
|
|
633
|
+
roles = role_list.map { ruby(_1) }.join(", ")
|
|
634
|
+
opts = []
|
|
635
|
+
opts << "create: true" if rule[:create]
|
|
636
|
+
opts << "delete: true" if rule[:delete]
|
|
637
|
+
read_val = reconstruct_access(rule.fetch(:default_rights, "None"), rule.fetch(:members, []), "ReadOnly", "ReadWrite")
|
|
638
|
+
write_val = reconstruct_access(rule.fetch(:default_rights, "None"), rule.fetch(:members, []), "ReadWrite", nil)
|
|
639
|
+
opts << "read: #{format_access(read_val)}" unless read_val == :none
|
|
640
|
+
opts << "write: #{format_access(write_val)}" unless write_val == :none
|
|
641
|
+
opts << "default_rights: #{ruby(rule.fetch(:default_rights, 'None'))}"
|
|
642
|
+
opts << "members: #{native_ruby(rule.fetch(:members, []))}"
|
|
643
|
+
xpath = rule.fetch(:xpath, "")
|
|
644
|
+
opts << "xpath: #{ruby(xpath)}" unless xpath.empty?
|
|
645
|
+
" access_rule #{[roles, *opts].join(', ')}"
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def reconstruct_access(default_rights, members, primary_rights, secondary_rights)
|
|
649
|
+
if default_rights == "ReadWrite"
|
|
650
|
+
return :all if primary_rights == "ReadWrite"
|
|
651
|
+
return :all if primary_rights == "ReadOnly"
|
|
652
|
+
end
|
|
653
|
+
if default_rights == "ReadOnly"
|
|
654
|
+
return :all if primary_rights == "ReadOnly"
|
|
655
|
+
explicit = members.select { _1[:rights] == primary_rights }.map { _1[:name] }
|
|
656
|
+
return explicit unless explicit.empty?
|
|
657
|
+
return :none
|
|
658
|
+
end
|
|
659
|
+
# default_rights == "None"
|
|
660
|
+
explicit = members.select { |m|
|
|
661
|
+
m[:rights] == primary_rights ||
|
|
662
|
+
(secondary_rights && m[:rights] == secondary_rights)
|
|
663
|
+
}.map { _1[:name] }
|
|
664
|
+
explicit.empty? ? :none : explicit
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def format_access(value)
|
|
668
|
+
case value
|
|
669
|
+
when :all, :none then ":#{value}"
|
|
670
|
+
when Array then "[#{value.map { symbol(_1) }.join(', ')}]"
|
|
671
|
+
else ":#{value}"
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def unique_entity_filenames(entities)
|
|
676
|
+
unique_filenames(entities)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def unique_filenames(entities)
|
|
680
|
+
used = {}
|
|
681
|
+
entities.to_h do |entity|
|
|
682
|
+
base = underscore(entity.name)
|
|
683
|
+
candidate = "#{base}.rb"
|
|
684
|
+
suffix = 2
|
|
685
|
+
while used[candidate]
|
|
686
|
+
candidate = "#{base}_#{suffix}.rb"
|
|
687
|
+
suffix += 1
|
|
688
|
+
end
|
|
689
|
+
used[candidate] = true
|
|
690
|
+
[entity.id, candidate]
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def underscore(value)
|
|
695
|
+
value.to_s
|
|
696
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
697
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
698
|
+
.tr(" -", "__")
|
|
699
|
+
.gsub(/[^a-zA-Z0-9_]/, "")
|
|
700
|
+
.downcase
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def microflow_source(flow, metadata = nil)
|
|
704
|
+
parameters = flow.parameters.filter_map do |param|
|
|
705
|
+
next unless param.is_a?(Hash)
|
|
706
|
+
name = param["Name"] || param["name"]
|
|
707
|
+
type = param["VariableType"] || param["Type"] || param["type"]
|
|
708
|
+
type_source = type.is_a?(Hash) ? native_ruby(type) : symbol(type)
|
|
709
|
+
" parameter #{symbol(name)}, type: #{type_source}" if name && type
|
|
710
|
+
end
|
|
711
|
+
body = parameters
|
|
712
|
+
body << " return_type #{symbol(flow.return_type)}" if flow.return_type.is_a?(String)
|
|
713
|
+
body << " documentation #{ruby(flow.documentation)}" unless flow.documentation.to_s.empty?
|
|
714
|
+
body << " allow_concurrent_execution #{flow.allow_concurrent_execution ? 'true' : 'false'}"
|
|
715
|
+
body << " mark_as_used #{flow.mark_as_used ? 'true' : 'false'}"
|
|
716
|
+
body << " excluded #{flow.excluded ? 'true' : 'false'}"
|
|
717
|
+
body << " allowed_roles #{flow.allowed_module_roles.map { symbol(_1) }.join(', ')}" unless flow.allowed_module_roles.empty?
|
|
718
|
+
metadata&.fetch(:calls, [])&.each do |call|
|
|
719
|
+
body << " call #{call.fetch(:kind)}: #{symbol(call.fetch(:name))}"
|
|
720
|
+
end
|
|
721
|
+
metadata&.fetch(:repositories, [])&.each do |repository|
|
|
722
|
+
body << " uses_repository #{symbol(repository)}"
|
|
723
|
+
end
|
|
724
|
+
objects = Array(flow.objects)
|
|
725
|
+
flows = Array(flow.flows)
|
|
726
|
+
if editable_flow_body?(objects, flows)
|
|
727
|
+
dsl_lines = body_dsl_lines(objects, flows, 2)
|
|
728
|
+
body.concat(dsl_lines)
|
|
729
|
+
fingerprint = flow_body_fingerprint(dsl_lines, flow)
|
|
730
|
+
body << " body_fingerprint #{ruby(fingerprint)}" if fingerprint
|
|
731
|
+
elsif objects.any?
|
|
732
|
+
body << " # Native body baseline retained: this graph shape has no typed DSL mapping yet"
|
|
733
|
+
end
|
|
734
|
+
<<~RUBY
|
|
735
|
+
# frozen_string_literal: true
|
|
736
|
+
|
|
737
|
+
microflow #{symbol(flow.name)} do
|
|
738
|
+
#{body.join("\n")}
|
|
739
|
+
end
|
|
740
|
+
RUBY
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def nanoflow_source(flow, metadata = nil)
|
|
744
|
+
microflow_source(flow, metadata).sub(/(^\s*)microflow /, '\1nanoflow ')
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def flow_body_fingerprint(lines, flow)
|
|
748
|
+
builder = Dsl::FlowBuilder.new(
|
|
749
|
+
flow.name, runtime: :server, kind: :microflow, public: false
|
|
750
|
+
)
|
|
751
|
+
builder.instance_eval(lines.join("\n"), @mpr_path, 1)
|
|
752
|
+
definition = builder.to_h
|
|
753
|
+
Dsl::FlowBuilder.body_digest(
|
|
754
|
+
definition[:body], definition[:return_expression]
|
|
755
|
+
)
|
|
756
|
+
rescue StandardError, SyntaxError
|
|
757
|
+
nil
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def repositories_source(repositories)
|
|
761
|
+
repositories.map do |repository|
|
|
762
|
+
options = []
|
|
763
|
+
options << "implementation: #{symbol(repository[:implementation])}" if repository[:implementation]
|
|
764
|
+
options << "public: true" if repository[:public]
|
|
765
|
+
options << "documentation: #{ruby(repository[:documentation])}" unless repository[:documentation].to_s.empty?
|
|
766
|
+
"repository #{symbol(repository.fetch(:name))}#{options.empty? ? '' : ", #{options.join(', ')}"}"
|
|
767
|
+
end.join("\n") + "\n"
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def page_source(page, metadata = nil)
|
|
771
|
+
body = []
|
|
772
|
+
body << " layout #{ruby(page.layout_id)}" if page.layout_id
|
|
773
|
+
body << " title #{ruby(page.title)}"
|
|
774
|
+
body << " popup!" if page.popup_width.to_i.positive? || page.popup_height.to_i.positive?
|
|
775
|
+
body << " allowed_roles #{page.allowed_module_roles.map { symbol(_1) }.join(', ')}" unless page.allowed_module_roles.empty?
|
|
776
|
+
deep = page_deep_structure(page)
|
|
777
|
+
if (source = metadata&.dig(:data_source) || page.data_source)
|
|
778
|
+
body << " data_source #{source.fetch(:kind)}: #{symbol(source.fetch(:name))}"
|
|
779
|
+
end
|
|
780
|
+
body << " deep_structure(#{native_ruby(deep, 2)})" if deep
|
|
781
|
+
metadata&.fetch(:events, [])&.each do |event|
|
|
782
|
+
args = []
|
|
783
|
+
args << "target: #{symbol(event[:target])}" if event[:target]
|
|
784
|
+
args << "#{event.fetch(:kind)}: #{symbol(event.fetch(:handler))}"
|
|
785
|
+
body << " #{event.fetch(:event)} #{args.join(', ')}"
|
|
786
|
+
end
|
|
787
|
+
widgets = metadata ? metadata.fetch(:widgets, []) : page.widgets
|
|
788
|
+
widgets.each do |widget|
|
|
789
|
+
rendered = render_widget(widget, 2)
|
|
790
|
+
body.concat(rendered)
|
|
791
|
+
end
|
|
792
|
+
<<~RUBY
|
|
793
|
+
# frozen_string_literal: true
|
|
794
|
+
|
|
795
|
+
page #{symbol(page.name)} do
|
|
796
|
+
#{body.join("\n")}
|
|
797
|
+
end
|
|
798
|
+
RUBY
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
PAGE_TYPED_KEYS = %w[$ID Name name].freeze
|
|
802
|
+
def page_deep_structure(page)
|
|
803
|
+
raw = page.raw_document
|
|
804
|
+
return nil unless raw.is_a?(Hash)
|
|
805
|
+
|
|
806
|
+
raw.reject { |key, _| PAGE_TYPED_KEYS.include?(key.to_s) }
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
def native_ruby(value, indent = 0)
|
|
810
|
+
case value
|
|
811
|
+
when BSON::Binary
|
|
812
|
+
encoded = Base64.strict_encode64(value.data)
|
|
813
|
+
"bson_binary(#{ruby(encoded)}, subtype: #{value.type.inspect})"
|
|
814
|
+
when Time
|
|
815
|
+
"Time.at(#{value.to_i}, #{value.nsec}, :nanosecond).utc"
|
|
816
|
+
when Hash
|
|
817
|
+
return "{}" if value.empty?
|
|
818
|
+
|
|
819
|
+
pad = " " * indent
|
|
820
|
+
child_pad = " " * (indent + 2)
|
|
821
|
+
entries = value.map do |key, item|
|
|
822
|
+
"#{child_pad}#{ruby(key)} => #{native_ruby(item, indent + 2)}"
|
|
823
|
+
end
|
|
824
|
+
"{\n#{entries.join(",\n")}\n#{pad}}"
|
|
825
|
+
when Array
|
|
826
|
+
return "[]" if value.empty?
|
|
827
|
+
|
|
828
|
+
pad = " " * indent
|
|
829
|
+
child_pad = " " * (indent + 2)
|
|
830
|
+
entries = value.map { "#{child_pad}#{native_ruby(_1, indent + 2)}" }
|
|
831
|
+
"[\n#{entries.join(",\n")}\n#{pad}]"
|
|
832
|
+
else
|
|
833
|
+
ruby(value)
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def menu_source(menu)
|
|
838
|
+
body = menu.items.flat_map { menu_item_source(_1, 2) }
|
|
839
|
+
if menu.raw_document.is_a?(Hash)
|
|
840
|
+
deep = menu.raw_document.reject { |key, _| %w[$ID Name name].include?(key.to_s) }
|
|
841
|
+
body.unshift(" deep_structure(#{native_ruby(deep, 2)})")
|
|
842
|
+
end
|
|
843
|
+
<<~RUBY
|
|
844
|
+
# frozen_string_literal: true
|
|
845
|
+
|
|
846
|
+
menu #{symbol(menu.name)} do
|
|
847
|
+
#{body.join("\n")}
|
|
848
|
+
end
|
|
849
|
+
RUBY
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def menu_item_source(item, indent)
|
|
853
|
+
pad = " " * indent
|
|
854
|
+
args = [ruby(item.fetch(:caption))]
|
|
855
|
+
args << "page: #{ruby(item[:page])}" if item[:page]
|
|
856
|
+
children = Array(item[:items])
|
|
857
|
+
return ["#{pad}item #{args.join(', ')}"] if children.empty?
|
|
858
|
+
|
|
859
|
+
lines = ["#{pad}item #{args.join(', ')} do"]
|
|
860
|
+
lines.concat(children.flat_map { menu_item_source(_1, indent + 2) })
|
|
861
|
+
lines << "#{pad}end"
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def render_widget(widget, indent)
|
|
865
|
+
pad = " " * indent
|
|
866
|
+
child_pad = " " * (indent + 2)
|
|
867
|
+
type = widget.fetch(:type)
|
|
868
|
+
options = widget.fetch(:options, {})
|
|
869
|
+
|
|
870
|
+
# Snippet: snippet :name, from: "..."
|
|
871
|
+
if type == :snippet
|
|
872
|
+
snippet_ref = options[:snippet].to_s
|
|
873
|
+
args = [ruby(widget.fetch(:name))]
|
|
874
|
+
args << "from: #{ruby(snippet_ref)}" unless snippet_ref == widget.fetch(:name).to_s
|
|
875
|
+
return ["#{pad}snippet #{args.join(', ')}"]
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
if type == :native_widget
|
|
879
|
+
return [
|
|
880
|
+
"#{pad}native_widget #{symbol(widget.fetch(:name))},",
|
|
881
|
+
"#{child_pad}type: #{ruby(options.fetch(:native_type))},",
|
|
882
|
+
"#{child_pad}deep_structure: #{native_ruby(options.fetch(:deep_structure), indent + 2)}"
|
|
883
|
+
]
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
if type == :pluggable_widget
|
|
887
|
+
args = [symbol(widget.fetch(:name)), "widget_id: #{ruby(options.fetch(:widget_id))}"]
|
|
888
|
+
args << "widget_name: #{ruby(options[:widget_name])}" if options[:widget_name]
|
|
889
|
+
args << "properties: #{native_ruby(options[:properties])}" if options[:properties]
|
|
890
|
+
args << "class_name: #{ruby(options[:class])}" if options[:class]
|
|
891
|
+
return ["#{pad}pluggable_widget #{args.join(', ')}"]
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# drop_down
|
|
895
|
+
if type == :drop_down
|
|
896
|
+
args = [symbol(widget.fetch(:name))]
|
|
897
|
+
args << "attribute: #{symbol(options[:attribute])}" if options[:attribute]
|
|
898
|
+
args << "caption: #{ruby(options[:caption])}" if options.key?(:caption) && !options[:caption].nil?
|
|
899
|
+
return ["#{pad}drop_down #{args.join(', ')}"]
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# Container: recurse into children
|
|
903
|
+
if type == :container
|
|
904
|
+
children_lines = Array(widget[:children]).flat_map { render_widget(_1, indent + 2) }
|
|
905
|
+
args = [symbol(widget.fetch(:name))]
|
|
906
|
+
args << "class_name: #{ruby(options[:class])}" if options[:class]
|
|
907
|
+
if children_lines.empty?
|
|
908
|
+
return ["#{pad}container #{args.join(', ')}"]
|
|
909
|
+
else
|
|
910
|
+
lines = ["#{pad}container #{args.join(', ')} do"]
|
|
911
|
+
lines.concat(children_lines)
|
|
912
|
+
lines << "#{pad}end"
|
|
913
|
+
return lines
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
args = [symbol(widget.fetch(:name))]
|
|
918
|
+
args << "attribute: #{symbol(options[:attribute])}" if options[:attribute]
|
|
919
|
+
args << "caption: #{ruby(options[:caption])}" if options.key?(:caption) && !options[:caption].nil?
|
|
920
|
+
args << "entity: #{ruby(options[:entity])}" if options[:entity]
|
|
921
|
+
args << "lines: #{options[:lines]}" if type == :text_area && options[:lines]
|
|
922
|
+
if type == :reference_selector && options[:display_attribute]
|
|
923
|
+
args << "display_attribute: #{ruby(options[:display_attribute])}"
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
widget_body = []
|
|
927
|
+
Array(options[:columns]).each do |column|
|
|
928
|
+
column_args = [symbol(column.fetch(:name))]
|
|
929
|
+
column_args << "attribute: #{symbol(column[:attribute])}" if column[:attribute]
|
|
930
|
+
column_args << "caption: #{ruby(column[:caption])}" if column.key?(:caption) && !column[:caption].nil?
|
|
931
|
+
widget_body << "#{child_pad}column #{column_args.join(', ')}"
|
|
932
|
+
end
|
|
933
|
+
Array(options[:tabs]).each do |tab|
|
|
934
|
+
tab_args = [symbol(tab.fetch(:name))]
|
|
935
|
+
tab_args << "caption: #{ruby(tab[:caption])}" if tab.key?(:caption) && !tab[:caption].nil?
|
|
936
|
+
tab_widgets = Array(tab[:widgets])
|
|
937
|
+
if tab_widgets.empty?
|
|
938
|
+
widget_body << "#{child_pad}tab_page #{tab_args.join(', ')}"
|
|
939
|
+
else
|
|
940
|
+
widget_body << "#{child_pad}tab_page #{tab_args.join(', ')} do"
|
|
941
|
+
widget_body.concat(tab_widgets.flat_map { render_widget(_1, indent + 4) })
|
|
942
|
+
widget_body << "#{child_pad}end"
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
if (sb = options[:search_bar])
|
|
947
|
+
sb_lines = ["#{child_pad}search_bar do"]
|
|
948
|
+
Array(sb[:fields]).each do |field|
|
|
949
|
+
sf_args = [symbol(field[:attribute])]
|
|
950
|
+
sf_args << "caption: #{ruby(field[:caption])}" if field[:caption]
|
|
951
|
+
sb_lines << "#{child_pad} search_field #{sf_args.join(', ')}"
|
|
952
|
+
end
|
|
953
|
+
sb_lines << "#{child_pad}end"
|
|
954
|
+
widget_body.concat(sb_lines)
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
if (tb = options[:toolbar])
|
|
958
|
+
tb_lines = ["#{child_pad}toolbar do"]
|
|
959
|
+
Array(tb[:buttons]).each do |btn|
|
|
960
|
+
btn_method = case btn[:type].to_sym
|
|
961
|
+
when :new then "new_button"
|
|
962
|
+
when :delete then "delete_button"
|
|
963
|
+
when :search then "search_button"
|
|
964
|
+
when :export then "export_button"
|
|
965
|
+
end
|
|
966
|
+
next unless btn_method
|
|
967
|
+
tb_arg = btn[:caption] ? "caption: #{ruby(btn[:caption])}" : nil
|
|
968
|
+
tb_lines << "#{child_pad} #{btn_method}#{tb_arg ? "(#{tb_arg})" : ""}"
|
|
969
|
+
end
|
|
970
|
+
tb_lines << "#{child_pad}end"
|
|
971
|
+
widget_body.concat(tb_lines)
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
widget.fetch(:events, []).each do |event|
|
|
975
|
+
widget_body << "#{child_pad}#{event.fetch(:event)} #{event.fetch(:kind)}: #{symbol(event.fetch(:handler))}"
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
if widget_body.empty?
|
|
979
|
+
["#{pad}#{type} #{args.join(', ')}"]
|
|
980
|
+
else
|
|
981
|
+
lines = ["#{pad}#{type} #{args.join(', ')} do"]
|
|
982
|
+
lines.concat(widget_body)
|
|
983
|
+
lines << "#{pad}end"
|
|
984
|
+
end
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
# ── Microflow / nanoflow body export ─────────────────────────────────────
|
|
988
|
+
|
|
989
|
+
def editable_flow_body?(objects, flows, nested: false)
|
|
990
|
+
return nested if objects.empty?
|
|
991
|
+
local_flows = flows_for_objects(flows, objects)
|
|
992
|
+
unless nested
|
|
993
|
+
return false unless objects.count { _1["$Type"] == "Microflows$StartEvent" } == 1
|
|
994
|
+
return false unless objects.any? { _1["$Type"] == "Microflows$EndEvent" }
|
|
995
|
+
end
|
|
996
|
+
connected_objects = objects.reject do |object|
|
|
997
|
+
%w[Microflows$Annotation Microflows$MicroflowParameter].include?(object["$Type"])
|
|
998
|
+
end
|
|
999
|
+
return false if connected_objects.size > 1 && local_flows.empty?
|
|
1000
|
+
return false unless objects.all? { EDITABLE_FLOW_OBJECT_TYPES.include?(_1["$Type"]) }
|
|
1001
|
+
return false unless local_flows.all? do
|
|
1002
|
+
%w[Microflows$SequenceFlow Microflows$AnnotationFlow].include?(_1["$Type"])
|
|
1003
|
+
end
|
|
1004
|
+
sequence_flows = local_flows.select { _1["$Type"] == "Microflows$SequenceFlow" }
|
|
1005
|
+
return false if sequence_flows.select { _1["IsErrorHandler"] == true }
|
|
1006
|
+
.group_by { _1["OriginPointer"] }.any? { |_origin, items| items.size > 1 }
|
|
1007
|
+
|
|
1008
|
+
by_id = objects.to_h { [_1["$ID"], _1] }
|
|
1009
|
+
|
|
1010
|
+
objects.all? do |object|
|
|
1011
|
+
case object["$Type"]
|
|
1012
|
+
when "Microflows$ActionActivity"
|
|
1013
|
+
editable_action?(object["Action"] || {})
|
|
1014
|
+
when "Microflows$ExclusiveSplit"
|
|
1015
|
+
decision_shape_editable?(object, sequence_flows, by_id)
|
|
1016
|
+
when "Microflows$InheritanceSplit"
|
|
1017
|
+
branches = sequence_flows.select { _1["OriginPointer"] == object["$ID"] }
|
|
1018
|
+
!object["SplitVariableName"].to_s.empty? &&
|
|
1019
|
+
branches.size >= 2 &&
|
|
1020
|
+
branches.all? { by_id.key?(_1["DestinationPointer"]) }
|
|
1021
|
+
when "Microflows$LoopedActivity"
|
|
1022
|
+
collection = object["ObjectCollection"] || {}
|
|
1023
|
+
inner_objects = bson_items(collection["Objects"])
|
|
1024
|
+
source = loop_source(object)
|
|
1025
|
+
return false unless %w[
|
|
1026
|
+
Microflows$WhileLoopCondition Microflows$IterableList
|
|
1027
|
+
Microflows$IteratorList
|
|
1028
|
+
].include?(source["$Type"])
|
|
1029
|
+
editable_flow_body?(
|
|
1030
|
+
inner_objects, flows, nested: true
|
|
1031
|
+
)
|
|
1032
|
+
else
|
|
1033
|
+
true
|
|
1034
|
+
end
|
|
1035
|
+
end
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def editable_action?(action)
|
|
1039
|
+
return false unless EDITABLE_ACTION_TYPES.include?(action["$Type"])
|
|
1040
|
+
|
|
1041
|
+
case action["$Type"]
|
|
1042
|
+
when "Microflows$ChangeObjectAction"
|
|
1043
|
+
action.fetch("Commit", "No") == "No" && action.fetch("RefreshInClient", false) == false
|
|
1044
|
+
when "Microflows$CreateChangeAction", "Microflows$ChangeAction"
|
|
1045
|
+
%w[No Yes YesWithoutEvents].include?(action.fetch("Commit", "No")) &&
|
|
1046
|
+
[true, false].include?(action.fetch("RefreshInClient", false))
|
|
1047
|
+
when "Microflows$RetrieveAction"
|
|
1048
|
+
source = action["RetrieveSource"] || {}
|
|
1049
|
+
case source["$Type"]
|
|
1050
|
+
when "Microflows$DatabaseRetrieveSource"
|
|
1051
|
+
range_type = source.dig("Range", "$Type")
|
|
1052
|
+
%w[Microflows$ConstantRange Microflows$CustomRange].include?(range_type) &&
|
|
1053
|
+
bson_items(source.dig("NewSortings", "Sortings") || source["SortItems"]).all? do |sorting|
|
|
1054
|
+
!(sorting["AttributePath"] || sorting.dig("AttributeRef", "Attribute")).to_s.empty?
|
|
1055
|
+
end
|
|
1056
|
+
when "Microflows$AssociationRetrieveSource"
|
|
1057
|
+
!source["AssociationId"].to_s.empty? && !source["StartVariableName"].to_s.empty?
|
|
1058
|
+
else
|
|
1059
|
+
false
|
|
1060
|
+
end
|
|
1061
|
+
when "Microflows$ShowMessageAction"
|
|
1062
|
+
bson_items(action.dig("Template", "Parameters")).all? { _1["Expression"] }
|
|
1063
|
+
when "Microflows$LogMessageAction"
|
|
1064
|
+
bson_items(action.dig("MessageTemplate", "Parameters")).all? { _1["Expression"] }
|
|
1065
|
+
when "Microflows$ShowFormAction"
|
|
1066
|
+
settings = action["FormSettings"] || {}
|
|
1067
|
+
title = settings["FormTitle"] || settings["TitleOverride"]
|
|
1068
|
+
title_supported = title.nil? ||
|
|
1069
|
+
(title.is_a?(Hash) && bson_items(title["Parameters"]).empty?)
|
|
1070
|
+
title_supported && bson_items(settings["ParameterMappings"]).all? do |mapping|
|
|
1071
|
+
mapping["Parameter"] && mapping["Argument"]
|
|
1072
|
+
end
|
|
1073
|
+
when "Microflows$JavaActionCallAction"
|
|
1074
|
+
!action["JavaAction"].to_s.empty? && bson_items(action["ParameterMappings"]).all? do |mapping|
|
|
1075
|
+
value = mapping["Value"] || {}
|
|
1076
|
+
mapping["Parameter"] && %w[
|
|
1077
|
+
Microflows$BasicJavaActionParameterValue
|
|
1078
|
+
Microflows$BasicCodeActionParameterValue
|
|
1079
|
+
Microflows$EntityTypeJavaActionParameterValue
|
|
1080
|
+
Microflows$MicroflowJavaActionParameterValue
|
|
1081
|
+
Microflows$ImportMappingJavaActionParameterValue
|
|
1082
|
+
Microflows$ExportMappingJavaActionParameterValue
|
|
1083
|
+
].include?(value["$Type"])
|
|
1084
|
+
end
|
|
1085
|
+
when "Microflows$JavaScriptActionCallAction"
|
|
1086
|
+
!action["JavaScriptAction"].to_s.empty? && bson_items(action["ParameterMappings"]).all? do |mapping|
|
|
1087
|
+
value = mapping["ParameterValue"] || {}
|
|
1088
|
+
mapping["Parameter"] && %w[
|
|
1089
|
+
Microflows$BasicCodeActionParameterValue
|
|
1090
|
+
Microflows$EntityTypeCodeActionParameterValue
|
|
1091
|
+
].include?(value["$Type"])
|
|
1092
|
+
end
|
|
1093
|
+
when "Microflows$NanoflowCallAction"
|
|
1094
|
+
!action.dig("NanoflowCall", "Nanoflow").to_s.empty? &&
|
|
1095
|
+
bson_items(action.dig("NanoflowCall", "ParameterMappings")).all? do |mapping|
|
|
1096
|
+
mapping["Parameter"] && mapping["Argument"]
|
|
1097
|
+
end
|
|
1098
|
+
when "Microflows$AppServiceCallAction"
|
|
1099
|
+
!action["AppServiceAction"].to_s.empty? && bson_items(action["ParameterMappings"]).all? do |mapping|
|
|
1100
|
+
mapping["Parameter"] && mapping["Argument"]
|
|
1101
|
+
end
|
|
1102
|
+
when "Microflows$ValidationFeedbackAction"
|
|
1103
|
+
bson_items(action.dig("FeedbackTemplate", "Parameters")).all? do |parameter|
|
|
1104
|
+
parameter["Expression"]
|
|
1105
|
+
end
|
|
1106
|
+
when "Microflows$RestCallAction"
|
|
1107
|
+
http = action["HttpConfiguration"] || {}
|
|
1108
|
+
request = action["RequestHandling"] || {}
|
|
1109
|
+
!http["HttpMethod"].to_s.empty? &&
|
|
1110
|
+
bson_items(http["HttpHeaderEntries"]).all? { _1["Key"] && _1["Value"] } &&
|
|
1111
|
+
bson_items(http.dig("CustomLocationTemplate", "Parameters")).all? {
|
|
1112
|
+
_1["Expression"]
|
|
1113
|
+
} &&
|
|
1114
|
+
request["$Type"] == "Microflows$MappingRequestHandling"
|
|
1115
|
+
else
|
|
1116
|
+
true
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
def decision_shape_editable?(object, flows, by_id)
|
|
1121
|
+
condition = object["SplitCondition"] || {}
|
|
1122
|
+
condition_supported = !condition["Expression"].to_s.empty? ||
|
|
1123
|
+
(condition["$Type"] == "Microflows$RuleSplitCondition" &&
|
|
1124
|
+
!condition.dig("RuleCall", "Microflow").to_s.empty? &&
|
|
1125
|
+
bson_items(condition.dig("RuleCall", "ParameterMappings")).all? {
|
|
1126
|
+
_1["Parameter"] && _1["Argument"]
|
|
1127
|
+
})
|
|
1128
|
+
return false unless condition_supported
|
|
1129
|
+
|
|
1130
|
+
branches = flows.reject { _1["IsErrorHandler"] == true }
|
|
1131
|
+
.select { _1["OriginPointer"] == object["$ID"] }
|
|
1132
|
+
branches.size >= 2 && branches.all? { by_id.key?(_1["DestinationPointer"]) }
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
def body_dsl_lines(objects, flows, indent = 2, nested: false)
|
|
1136
|
+
return [] if objects.empty?
|
|
1137
|
+
by_id = objects.to_h { |o| [o["$ID"], o] }
|
|
1138
|
+
fwd_map = {}
|
|
1139
|
+
err_map = {}
|
|
1140
|
+
flows.each do |f|
|
|
1141
|
+
next unless f["$Type"] == "Microflows$SequenceFlow"
|
|
1142
|
+
|
|
1143
|
+
from = f["OriginPointer"]
|
|
1144
|
+
to = f["DestinationPointer"]
|
|
1145
|
+
case_val = flow_case_value(f)
|
|
1146
|
+
if f["IsErrorHandler"] == true
|
|
1147
|
+
err_map[from] = to
|
|
1148
|
+
else
|
|
1149
|
+
fwd_map[from] ||= []
|
|
1150
|
+
fwd_map[from] << { to: to, case_val: case_val }
|
|
1151
|
+
end
|
|
1152
|
+
end
|
|
1153
|
+
start = objects.find { |o| o["$Type"] == "Microflows$StartEvent" }
|
|
1154
|
+
if nested && !start
|
|
1155
|
+
destinations = flows.to_h { [_1["DestinationPointer"], true] }
|
|
1156
|
+
start = objects.find { !destinations.key?(_1["$ID"]) }
|
|
1157
|
+
end
|
|
1158
|
+
return [] unless start
|
|
1159
|
+
lines = []
|
|
1160
|
+
linearize_flow(start["$ID"], fwd_map, err_map, by_id, flows, {}, lines, indent, nil)
|
|
1161
|
+
lines
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
def flow_case_value(flow)
|
|
1165
|
+
case_doc = bson_items(flow["CaseValues"]).first || flow["NewCaseValue"]
|
|
1166
|
+
value = case_doc&.dig("Value")
|
|
1167
|
+
return true if value == "true"
|
|
1168
|
+
return false if value == "false"
|
|
1169
|
+
|
|
1170
|
+
value
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
def linearize_flow(cursor, fwd, err, by_id, graph_flows, seen, lines, indent, stop_at)
|
|
1174
|
+
while cursor && cursor != stop_at
|
|
1175
|
+
break if seen[cursor]
|
|
1176
|
+
seen[cursor] = true
|
|
1177
|
+
obj = by_id[cursor]
|
|
1178
|
+
break unless obj
|
|
1179
|
+
|
|
1180
|
+
case obj["$Type"]
|
|
1181
|
+
when "Microflows$StartEvent"
|
|
1182
|
+
cursor = (fwd[cursor] || []).first&.dig(:to)
|
|
1183
|
+
|
|
1184
|
+
when "Microflows$ActionActivity"
|
|
1185
|
+
line = action_dsl_line(obj, indent)
|
|
1186
|
+
lines << line if line
|
|
1187
|
+
next_id = (fwd[cursor] || []).first&.dig(:to)
|
|
1188
|
+
error_to = err[cursor]
|
|
1189
|
+
if error_to
|
|
1190
|
+
rescue_lines = []
|
|
1191
|
+
linearize_flow(
|
|
1192
|
+
error_to, fwd, err, by_id, graph_flows,
|
|
1193
|
+
seen.dup, rescue_lines, indent + 2, nil
|
|
1194
|
+
)
|
|
1195
|
+
lines << "#{' ' * indent}rescue_all do"
|
|
1196
|
+
lines.concat(rescue_lines)
|
|
1197
|
+
lines << "#{' ' * indent}end"
|
|
1198
|
+
end
|
|
1199
|
+
cursor = next_id
|
|
1200
|
+
|
|
1201
|
+
when "Microflows$ExclusiveSplit"
|
|
1202
|
+
condition_doc = obj["SplitCondition"] || {}
|
|
1203
|
+
condition = if condition_doc["$Type"] == "Microflows$RuleSplitCondition"
|
|
1204
|
+
rule = condition_doc["RuleCall"] || {}
|
|
1205
|
+
mappings = bson_items(rule["ParameterMappings"]).to_h do |mapping|
|
|
1206
|
+
[mapping["Parameter"].to_s, mapping["Argument"].to_s]
|
|
1207
|
+
end
|
|
1208
|
+
{ rule: rule["Microflow"], pass: mappings }
|
|
1209
|
+
else
|
|
1210
|
+
condition_doc["Expression"].to_s
|
|
1211
|
+
end
|
|
1212
|
+
nexts = fwd[cursor] || []
|
|
1213
|
+
merge_id = find_common_merge_node(nexts.map { _1[:to] }, fwd, by_id)
|
|
1214
|
+
rendered_condition = condition.is_a?(Hash) ?
|
|
1215
|
+
"(#{ruby(condition)})" : ruby(condition)
|
|
1216
|
+
lines << "#{' ' * indent}decision #{rendered_condition} do"
|
|
1217
|
+
nexts.each do |edge|
|
|
1218
|
+
branch_lines = []
|
|
1219
|
+
linearize_flow(
|
|
1220
|
+
edge[:to], fwd, err, by_id, graph_flows, seen.dup,
|
|
1221
|
+
branch_lines, indent + 4, merge_id
|
|
1222
|
+
)
|
|
1223
|
+
lines << "#{' ' * (indent + 2)}on(#{ruby(edge[:case_val])}) do"
|
|
1224
|
+
lines.concat(branch_lines)
|
|
1225
|
+
lines << "#{' ' * (indent + 2)}end"
|
|
1226
|
+
end
|
|
1227
|
+
lines << "#{' ' * indent}end"
|
|
1228
|
+
cursor = merge_id
|
|
1229
|
+
|
|
1230
|
+
when "Microflows$ExclusiveMerge"
|
|
1231
|
+
cursor = (fwd[cursor] || []).first&.dig(:to)
|
|
1232
|
+
|
|
1233
|
+
when "Microflows$InheritanceSplit"
|
|
1234
|
+
nexts = fwd[cursor] || []
|
|
1235
|
+
merge_id = find_common_merge_node(nexts.map { _1[:to] }, fwd, by_id)
|
|
1236
|
+
lines << "#{' ' * indent}type_decision :#{obj["SplitVariableName"]} do"
|
|
1237
|
+
nexts.each do |edge|
|
|
1238
|
+
branch_lines = []
|
|
1239
|
+
linearize_flow(
|
|
1240
|
+
edge[:to], fwd, err, by_id, graph_flows, seen.dup,
|
|
1241
|
+
branch_lines, indent + 4, merge_id
|
|
1242
|
+
)
|
|
1243
|
+
if edge[:case_val].to_s.empty?
|
|
1244
|
+
lines << "#{' ' * (indent + 2)}otherwise do"
|
|
1245
|
+
else
|
|
1246
|
+
lines << "#{' ' * (indent + 2)}on_type #{ruby(edge[:case_val])} do"
|
|
1247
|
+
end
|
|
1248
|
+
lines.concat(branch_lines)
|
|
1249
|
+
lines << "#{' ' * (indent + 2)}end"
|
|
1250
|
+
end
|
|
1251
|
+
lines << "#{' ' * indent}end"
|
|
1252
|
+
cursor = merge_id
|
|
1253
|
+
|
|
1254
|
+
when "Microflows$LoopedActivity"
|
|
1255
|
+
loop_src = loop_source(obj)
|
|
1256
|
+
variable = (loop_src["ListVariableName"] || loop_src["Variable"]).to_s
|
|
1257
|
+
iterator = (loop_src["VariableName"] || loop_src["IteratorVariable"]).to_s
|
|
1258
|
+
inner_col = obj["ObjectCollection"] || {}
|
|
1259
|
+
inner_objects = bson_items(inner_col["Objects"])
|
|
1260
|
+
inner_lines = body_dsl_lines(
|
|
1261
|
+
inner_objects,
|
|
1262
|
+
graph_flows,
|
|
1263
|
+
indent + 2,
|
|
1264
|
+
nested: true
|
|
1265
|
+
)
|
|
1266
|
+
if loop_src["$Type"] == "Microflows$WhileLoopCondition"
|
|
1267
|
+
lines << "#{' ' * indent}while_loop #{ruby(loop_src["WhileExpression"])} do"
|
|
1268
|
+
else
|
|
1269
|
+
as_clause = (iterator != variable && !iterator.empty?) ? ", as: :#{iterator}" : ""
|
|
1270
|
+
lines << "#{' ' * indent}loop_over :#{variable}#{as_clause} do"
|
|
1271
|
+
end
|
|
1272
|
+
lines.concat(inner_lines)
|
|
1273
|
+
lines << "#{' ' * indent}end"
|
|
1274
|
+
cursor = (fwd[cursor] || []).first&.dig(:to)
|
|
1275
|
+
|
|
1276
|
+
when "Microflows$EndEvent"
|
|
1277
|
+
ret = obj["ReturnValue"].to_s.strip
|
|
1278
|
+
unless ret.empty?
|
|
1279
|
+
if (match = ret.match(/\A\$([A-Za-z_][A-Za-z0-9_]*)\z/))
|
|
1280
|
+
lines << "#{' ' * indent}return_value :#{match[1]}"
|
|
1281
|
+
else
|
|
1282
|
+
lines << "#{' ' * indent}return_value #{ruby(ret)}"
|
|
1283
|
+
end
|
|
1284
|
+
else
|
|
1285
|
+
lines << "#{' ' * indent}end_flow" if indent > 2
|
|
1286
|
+
end
|
|
1287
|
+
break
|
|
1288
|
+
|
|
1289
|
+
when "Microflows$ErrorEvent"
|
|
1290
|
+
lines << "#{' ' * indent}error_event"
|
|
1291
|
+
break
|
|
1292
|
+
|
|
1293
|
+
when "Microflows$ContinueEvent"
|
|
1294
|
+
lines << "#{' ' * indent}continue_loop"
|
|
1295
|
+
break
|
|
1296
|
+
|
|
1297
|
+
else
|
|
1298
|
+
cursor = (fwd[cursor] || []).first&.dig(:to)
|
|
1299
|
+
end
|
|
1300
|
+
end
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1303
|
+
def flows_for_objects(flows, objects)
|
|
1304
|
+
ids = objects.to_h { [_1["$ID"], true] }
|
|
1305
|
+
flows.select do |flow|
|
|
1306
|
+
ids.key?(flow["OriginPointer"]) && ids.key?(flow["DestinationPointer"])
|
|
1307
|
+
end
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
def loop_source(object)
|
|
1311
|
+
object["LoopSource"] || object["LoopObject"] || begin
|
|
1312
|
+
if object["ListVariableName"] || object["IteratorVariableName"]
|
|
1313
|
+
{
|
|
1314
|
+
"$Type" => "Microflows$IterableList",
|
|
1315
|
+
"ListVariableName" => object["ListVariableName"],
|
|
1316
|
+
"VariableName" => object["IteratorVariableName"]
|
|
1317
|
+
}
|
|
1318
|
+
else
|
|
1319
|
+
{}
|
|
1320
|
+
end
|
|
1321
|
+
end
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
def find_merge_node(true_start, false_start, fwd, by_id)
|
|
1325
|
+
t = reachable_merge_ids(true_start, fwd, by_id)
|
|
1326
|
+
f = reachable_merge_ids(false_start, fwd, by_id)
|
|
1327
|
+
(t & f).first
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
def find_common_merge_node(starts, fwd, by_id)
|
|
1331
|
+
sets = starts.compact.map { reachable_merge_ids(_1, fwd, by_id) }
|
|
1332
|
+
return nil if sets.empty?
|
|
1333
|
+
|
|
1334
|
+
sets.reduce { |common, ids| common & ids }.first
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
def reachable_merge_ids(start_id, fwd, by_id)
|
|
1338
|
+
result = []
|
|
1339
|
+
visited = {}
|
|
1340
|
+
queue = [start_id].compact
|
|
1341
|
+
while (id = queue.shift)
|
|
1342
|
+
next if visited[id]
|
|
1343
|
+
visited[id] = true
|
|
1344
|
+
obj = by_id[id]
|
|
1345
|
+
next unless obj
|
|
1346
|
+
result << id if obj["$Type"] == "Microflows$ExclusiveMerge"
|
|
1347
|
+
(fwd[id] || []).each { |e| queue << e[:to] }
|
|
1348
|
+
end
|
|
1349
|
+
result
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
def action_dsl_line(obj, indent)
|
|
1353
|
+
action = obj["Action"] || {}
|
|
1354
|
+
pad = " " * indent
|
|
1355
|
+
case action["$Type"]
|
|
1356
|
+
when "Microflows$CreateObjectAction", "Microflows$CreateChangeAction"
|
|
1357
|
+
legacy = action["$Type"] == "Microflows$CreateChangeAction"
|
|
1358
|
+
members = member_specs(action[legacy ? "Items" : "Members"])
|
|
1359
|
+
has_associations = members.any? { !_1[:association].to_s.empty? }
|
|
1360
|
+
set_arg = members.empty? || has_associations ?
|
|
1361
|
+
"" : ", set: { #{members_dsl(members)} }"
|
|
1362
|
+
variable = legacy ? action["VariableName"] : action["OutputVariableName"]
|
|
1363
|
+
commit_a = action["Commit"] == "Yes" ? ", commit: true" : ""
|
|
1364
|
+
commit_a = ", commit: true, with_events: false" if action["Commit"] == "YesWithoutEvents"
|
|
1365
|
+
refresh_a = action["RefreshInClient"] == true ? ", refresh: true" : ""
|
|
1366
|
+
command = "#{pad}create_object #{ruby(action["Entity"])}, as: :#{variable}#{set_arg}#{commit_a}#{refresh_a}"
|
|
1367
|
+
has_associations ? member_block(command, members, indent) : command
|
|
1368
|
+
when "Microflows$ChangeObjectAction", "Microflows$ChangeAction"
|
|
1369
|
+
legacy = action["$Type"] == "Microflows$ChangeAction"
|
|
1370
|
+
members = member_specs(action[legacy ? "Items" : "Members"])
|
|
1371
|
+
variable = legacy ? action["ChangeVariableName"] : action["Variable"]
|
|
1372
|
+
commit_a = action["Commit"] == "Yes" ? ", commit: true" : ""
|
|
1373
|
+
commit_a = ", commit: true, with_events: false" if action["Commit"] == "YesWithoutEvents"
|
|
1374
|
+
refresh_a = action["RefreshInClient"] == true ? ", refresh: true" : ""
|
|
1375
|
+
has_associations = members.any? { !_1[:association].to_s.empty? }
|
|
1376
|
+
set_arg = members.empty? || has_associations ?
|
|
1377
|
+
"" : ", set: { #{members_dsl(members)} }"
|
|
1378
|
+
command = "#{pad}change_object :#{variable}#{set_arg}#{commit_a}#{refresh_a}"
|
|
1379
|
+
has_associations ? member_block(command, members, indent) : command
|
|
1380
|
+
when "Microflows$RetrieveAction"
|
|
1381
|
+
src = action["RetrieveSource"] || {}
|
|
1382
|
+
variable = action["ResultVariableName"] || action["ResultListName"]
|
|
1383
|
+
if src["$Type"] == "Microflows$AssociationRetrieveSource"
|
|
1384
|
+
"#{pad}retrieve_association :#{src["StartVariableName"]}, " \
|
|
1385
|
+
"association: #{ruby(src["AssociationId"])}, as: :#{variable}"
|
|
1386
|
+
else
|
|
1387
|
+
xpath = (src["XpathConstraint"] || src["XPath"]).to_s
|
|
1388
|
+
args = [ruby(src["Entity"]), "as: :#{variable}"]
|
|
1389
|
+
args << "xpath: #{ruby(xpath)}" unless xpath.empty?
|
|
1390
|
+
range = src["Range"] || {}
|
|
1391
|
+
if range["$Type"] == "Microflows$CustomRange"
|
|
1392
|
+
limit = range["LimitExpression"].to_s
|
|
1393
|
+
args << "limit: #{ruby(limit)}" unless limit.empty?
|
|
1394
|
+
elsif range["SingleObject"] == true
|
|
1395
|
+
args << "single: true"
|
|
1396
|
+
end
|
|
1397
|
+
sortings = bson_items(src.dig("NewSortings", "Sortings") || src["SortItems"])
|
|
1398
|
+
unless sortings.empty?
|
|
1399
|
+
rendered = sortings.map do |sorting|
|
|
1400
|
+
attribute = sorting["AttributePath"] ||
|
|
1401
|
+
sorting.dig("AttributeRef", "Attribute")
|
|
1402
|
+
"[#{ruby(attribute)}, :#{underscore(sorting["SortOrder"])}]"
|
|
1403
|
+
end
|
|
1404
|
+
args << "sort: [#{rendered.join(', ')}]"
|
|
1405
|
+
end
|
|
1406
|
+
"#{pad}retrieve_objects #{args.join(', ')}"
|
|
1407
|
+
end
|
|
1408
|
+
when "Microflows$CommitObjectsAction", "Microflows$CommitAction"
|
|
1409
|
+
legacy = action["$Type"] == "Microflows$CommitAction"
|
|
1410
|
+
variable = legacy ? action["CommitVariableName"] : action["Variable"]
|
|
1411
|
+
with_events = legacy ? action.fetch("WithEvents", true) : action["CommitWithoutEvents"] != true
|
|
1412
|
+
events_a = with_events ? "" : ", with_events: false"
|
|
1413
|
+
refresh_a = action["RefreshInClient"] == true ? ", refresh: true" : ""
|
|
1414
|
+
"#{pad}commit :#{variable}#{events_a}#{refresh_a}"
|
|
1415
|
+
when "Microflows$DeleteAction"
|
|
1416
|
+
variable = action["DeleteVariableName"] || action["DeleteVariable"]
|
|
1417
|
+
refresh_a = action["RefreshInClient"] == true ? ", refresh: true" : ""
|
|
1418
|
+
"#{pad}delete :#{variable}#{refresh_a}"
|
|
1419
|
+
when "Microflows$MicroflowCallAction"
|
|
1420
|
+
call = action["MicroflowCall"] || {}
|
|
1421
|
+
out = if action["UseReturnVariable"] == true
|
|
1422
|
+
(action["ResultVariableName"] || action["OutputVariableName"]).to_s
|
|
1423
|
+
else
|
|
1424
|
+
""
|
|
1425
|
+
end
|
|
1426
|
+
as_a = out.empty? ? "" : ", as: :#{out}"
|
|
1427
|
+
stored_result = (action["ResultVariableName"] || action["OutputVariableName"]).to_s
|
|
1428
|
+
result_a = if out.empty? && !stored_result.empty?
|
|
1429
|
+
", result_name: #{symbol(stored_result)}, use_return: false"
|
|
1430
|
+
elsif action["UseReturnVariable"] == true && out.empty?
|
|
1431
|
+
", use_return: true"
|
|
1432
|
+
else
|
|
1433
|
+
""
|
|
1434
|
+
end
|
|
1435
|
+
params = bson_items(call["ParameterMappings"]).map { [_1["Parameter"], _1["Argument"]] }
|
|
1436
|
+
pass_a = params.empty? ? "" : ", pass: #{pass_source(params)}"
|
|
1437
|
+
"#{pad}call_microflow #{ruby(call["Microflow"])}#{as_a}#{pass_a}#{result_a}"
|
|
1438
|
+
when "Microflows$CreateVariableAction"
|
|
1439
|
+
type = action.dig("VariableType", "$Type").to_s
|
|
1440
|
+
type = type.delete_prefix("DataTypes$").delete_suffix("Type")
|
|
1441
|
+
value = action["InitialValue"]
|
|
1442
|
+
value_a = value.nil? || value.to_s.empty? ? "" : ", value: #{ruby_val(value)}"
|
|
1443
|
+
type_a = type.empty? ? "" : ", type: #{symbol(type)}"
|
|
1444
|
+
"#{pad}create_variable :#{action["VariableName"]}#{type_a}#{value_a}"
|
|
1445
|
+
when "Microflows$ChangeVariableAction"
|
|
1446
|
+
"#{pad}change_variable :#{action["ChangeVariableName"]}, to: #{ruby_val(action["Value"])}"
|
|
1447
|
+
when "Microflows$ShowMessageAction"
|
|
1448
|
+
template = action["Template"] || {}
|
|
1449
|
+
translations = translated_items(template["Text"]).to_h do |translation|
|
|
1450
|
+
[translation["LanguageCode"].to_s, translation["Text"].to_s]
|
|
1451
|
+
end
|
|
1452
|
+
parameters = bson_items(template["Parameters"]).map { _1["Expression"] }
|
|
1453
|
+
text = translations["en_US"] || translations.values.first.to_s
|
|
1454
|
+
type_a = action["Type"].to_s.empty? ? "" : ", type: :#{underscore(action["Type"])}"
|
|
1455
|
+
blocking_a = action["Blocking"] == true ? ", blocking: true" : ""
|
|
1456
|
+
translations_a = translations.size > 1 ? ", translations: #{ruby(translations)}" : ""
|
|
1457
|
+
parameters_a = parameters.empty? ? "" : ", parameters: #{ruby(parameters)}"
|
|
1458
|
+
"#{pad}show_message #{ruby(text)}#{type_a}#{blocking_a}#{translations_a}#{parameters_a}"
|
|
1459
|
+
when "Microflows$LogMessageAction"
|
|
1460
|
+
template = action["MessageTemplate"] || {}
|
|
1461
|
+
parameters = bson_items(template["Parameters"]).map { _1["Expression"] }
|
|
1462
|
+
level_a = action["Level"].to_s.empty? ? "" : ", level: :#{underscore(action["Level"])}"
|
|
1463
|
+
node_a = action["Node"].to_s.empty? ? "" : ", node: #{ruby(action["Node"])}"
|
|
1464
|
+
stack_a = action["IncludeLatestStackTrace"] == true ? ", include_stack: true" : ""
|
|
1465
|
+
parameters_a = parameters.empty? ? "" : ", parameters: #{ruby(parameters)}"
|
|
1466
|
+
"#{pad}log_message #{ruby(template["Text"].to_s)}#{level_a}#{node_a}#{stack_a}#{parameters_a}"
|
|
1467
|
+
when "Microflows$ShowFormAction"
|
|
1468
|
+
settings = action["FormSettings"] || {}
|
|
1469
|
+
args = [ruby(settings["Form"])]
|
|
1470
|
+
variable = action["FormObjectVariable"].to_s
|
|
1471
|
+
args << "object: :#{variable}" unless variable.empty?
|
|
1472
|
+
location = settings["Location"].to_s
|
|
1473
|
+
args << "location: :#{underscore(location)}" unless location.empty?
|
|
1474
|
+
mappings = bson_items(settings["ParameterMappings"]).map do |mapping|
|
|
1475
|
+
"#{ruby(mapping["Parameter"])} => #{ruby_val(mapping["Argument"])}"
|
|
1476
|
+
end
|
|
1477
|
+
args << "pass: { #{mappings.join(', ')} }" unless mappings.empty?
|
|
1478
|
+
close_pages = action["NumberOfPagesToClose"].to_s
|
|
1479
|
+
args << "close_pages: #{close_pages.to_i}" unless close_pages.empty?
|
|
1480
|
+
title = settings["FormTitle"] || settings["TitleOverride"]
|
|
1481
|
+
translations = translated_items(title&.fetch("Text", title)).to_h do |translation|
|
|
1482
|
+
[translation["LanguageCode"].to_s, translation["Text"].to_s]
|
|
1483
|
+
end
|
|
1484
|
+
args << "title: #{ruby(translations)}" unless translations.empty?
|
|
1485
|
+
"#{pad}show_page #{args.join(', ')}"
|
|
1486
|
+
when "Microflows$CloseFormAction"
|
|
1487
|
+
count = action["NumberOfPagesToClose"].to_s
|
|
1488
|
+
count.empty? ? "#{pad}close_page" : "#{pad}close_page count: #{count.to_i}"
|
|
1489
|
+
when "Microflows$JavaActionCallAction"
|
|
1490
|
+
call_action_line(
|
|
1491
|
+
pad, "call_java", action["JavaAction"], action["ResultVariableName"],
|
|
1492
|
+
bson_items(action["ParameterMappings"]).map {
|
|
1493
|
+
[_1["Parameter"], code_action_parameter_value(_1["Value"])]
|
|
1494
|
+
},
|
|
1495
|
+
action["UseReturnVariable"]
|
|
1496
|
+
)
|
|
1497
|
+
when "Microflows$JavaScriptActionCallAction"
|
|
1498
|
+
call_action_line(
|
|
1499
|
+
pad, "call_javascript", action["JavaScriptAction"], action["OutputVariableName"],
|
|
1500
|
+
bson_items(action["ParameterMappings"]).map {
|
|
1501
|
+
[_1["Parameter"], code_action_parameter_value(_1["ParameterValue"])]
|
|
1502
|
+
},
|
|
1503
|
+
action["UseReturnVariable"]
|
|
1504
|
+
)
|
|
1505
|
+
when "Microflows$NanoflowCallAction"
|
|
1506
|
+
call = action["NanoflowCall"] || {}
|
|
1507
|
+
call_action_line(
|
|
1508
|
+
pad, "call_nanoflow", call["Nanoflow"], action["OutputVariableName"],
|
|
1509
|
+
bson_items(call["ParameterMappings"]).map { [_1["Parameter"], _1["Argument"]] },
|
|
1510
|
+
action["UseReturnVariable"]
|
|
1511
|
+
)
|
|
1512
|
+
when "Microflows$AppServiceCallAction"
|
|
1513
|
+
call_action_line(
|
|
1514
|
+
pad, "call_app_service", action["AppServiceAction"], action["ResultVariableName"],
|
|
1515
|
+
bson_items(action["ParameterMappings"]).map { [_1["Parameter"], _1["Argument"]] },
|
|
1516
|
+
action["UseReturnVariable"]
|
|
1517
|
+
)
|
|
1518
|
+
when "Microflows$AggregateAction"
|
|
1519
|
+
args = [
|
|
1520
|
+
":#{action["AggregateVariableName"]}",
|
|
1521
|
+
"function: :#{underscore(action["AggregateFunction"])}",
|
|
1522
|
+
"as: :#{action["VariableName"]}"
|
|
1523
|
+
]
|
|
1524
|
+
args << "attribute: #{ruby(action["Attribute"])}" unless action["Attribute"].to_s.empty?
|
|
1525
|
+
"#{pad}aggregate #{args.join(', ')}"
|
|
1526
|
+
when "Microflows$RollbackAction"
|
|
1527
|
+
refresh = action["RefreshInClient"] == true ? ", refresh: true" : ""
|
|
1528
|
+
"#{pad}rollback :#{action["RollbackVariableName"]}#{refresh}"
|
|
1529
|
+
when "Microflows$CastAction"
|
|
1530
|
+
"#{pad}cast :#{action["VariableName"]}"
|
|
1531
|
+
when "Microflows$CreateListAction"
|
|
1532
|
+
"#{pad}create_list #{ruby(action["Entity"])}, as: :#{action["VariableName"]}"
|
|
1533
|
+
when "Microflows$ListOperationsAction"
|
|
1534
|
+
operation = action["NewOperation"] || {}
|
|
1535
|
+
op = operation["$Type"].to_s.delete_prefix("Microflows$")
|
|
1536
|
+
args = [":#{underscore(op)}", ":#{operation['ListName']}"]
|
|
1537
|
+
second = operation["SecondListOrObjectName"].to_s
|
|
1538
|
+
expression = operation["Expression"]
|
|
1539
|
+
args << "with: :#{second}" unless second.empty?
|
|
1540
|
+
args << "expression: #{ruby(expression)}" unless expression.nil?
|
|
1541
|
+
args << "as: :#{action['ResultVariableName']}"
|
|
1542
|
+
"#{pad}list_operation #{args.join(', ')}"
|
|
1543
|
+
when "Microflows$ChangeListAction"
|
|
1544
|
+
"#{pad}change_list :#{action["ChangeVariableName"]}, " \
|
|
1545
|
+
"action: :#{underscore(action["Type"])}, value: #{ruby_val(action["Value"])}"
|
|
1546
|
+
when "Microflows$ValidationFeedbackAction"
|
|
1547
|
+
template = action["FeedbackTemplate"] || {}
|
|
1548
|
+
translations = translated_items(template["Text"]).to_h do |translation|
|
|
1549
|
+
[translation["LanguageCode"].to_s, translation["Text"].to_s]
|
|
1550
|
+
end
|
|
1551
|
+
parameters = bson_items(template["Parameters"]).map { _1["Expression"] }
|
|
1552
|
+
args = [":#{action["ValidationVariableName"]}"]
|
|
1553
|
+
args << "attribute: #{ruby(action["Attribute"])}" unless action["Attribute"].to_s.empty?
|
|
1554
|
+
args << "association: #{ruby(action["Association"])}" unless action["Association"].to_s.empty?
|
|
1555
|
+
args << "translations: #{ruby(translations)}"
|
|
1556
|
+
args << "parameters: #{ruby(parameters)}" unless parameters.empty?
|
|
1557
|
+
error = underscore(action["ErrorHandlingType"])
|
|
1558
|
+
args << "error: :#{error}" unless error == "rollback"
|
|
1559
|
+
"#{pad}validation_feedback #{args.join(', ')}"
|
|
1560
|
+
when "Microflows$RestCallAction"
|
|
1561
|
+
rest_call_line(pad, action)
|
|
1562
|
+
end
|
|
1563
|
+
end
|
|
1564
|
+
|
|
1565
|
+
def rest_call_line(pad, action)
|
|
1566
|
+
http = action["HttpConfiguration"] || {}
|
|
1567
|
+
location = http["CustomLocationTemplate"] || {}
|
|
1568
|
+
headers = bson_items(http["HttpHeaderEntries"]).to_h do |header|
|
|
1569
|
+
[header["Key"].to_s, header["Value"].to_s]
|
|
1570
|
+
end
|
|
1571
|
+
request = action["RequestHandling"] || {}
|
|
1572
|
+
result = action["ResultHandling"] || {}
|
|
1573
|
+
import_call = result["ImportMappingCall"] || {}
|
|
1574
|
+
args = [
|
|
1575
|
+
"method: :#{underscore(http["HttpMethod"])}",
|
|
1576
|
+
"location: #{ruby(location["Text"].to_s)}"
|
|
1577
|
+
]
|
|
1578
|
+
parameters = bson_items(location["Parameters"]).map { _1["Expression"] }
|
|
1579
|
+
args << "location_parameters: #{ruby(parameters)}" unless parameters.empty?
|
|
1580
|
+
args << "headers: #{ruby(headers)}" unless headers.empty?
|
|
1581
|
+
args << "request_mapping: #{ruby(request["MappingId"])}" if request["MappingId"]
|
|
1582
|
+
args << "request_variable: :#{request["MappingVariableName"]}" if request["MappingVariableName"]
|
|
1583
|
+
args << "result_mapping: #{ruby(import_call["ReturnValueMapping"])}" if import_call["ReturnValueMapping"]
|
|
1584
|
+
args << "as: :#{result["ResultVariableName"]}" unless result["ResultVariableName"].to_s.empty?
|
|
1585
|
+
args << "result_entity: #{ruby(result.dig("VariableType", "Entity"))}" if result.dig("VariableType", "Entity")
|
|
1586
|
+
args << "timeout: #{ruby(action["TimeOutExpression"])}" if action["UseRequestTimeOut"] == true
|
|
1587
|
+
args << "commit: :#{underscore(import_call["Commit"])}" unless import_call["Commit"].to_s.empty?
|
|
1588
|
+
args << "error_result: :#{underscore(action["ErrorResultHandlingType"])}"
|
|
1589
|
+
args << "error: :#{underscore(action["ErrorHandlingType"])}"
|
|
1590
|
+
"#{pad}call_rest #{args.join(', ')}"
|
|
1591
|
+
end
|
|
1592
|
+
|
|
1593
|
+
def call_action_line(pad, method, target, output, mappings, use_return)
|
|
1594
|
+
args = [ruby(target)]
|
|
1595
|
+
if use_return == true && !output.to_s.empty?
|
|
1596
|
+
args << "as: :#{output}"
|
|
1597
|
+
elsif !output.to_s.empty?
|
|
1598
|
+
args << "result_name: #{symbol(output)}"
|
|
1599
|
+
args << "use_return: false"
|
|
1600
|
+
elsif use_return == true
|
|
1601
|
+
args << "use_return: true"
|
|
1602
|
+
end
|
|
1603
|
+
unless mappings.empty?
|
|
1604
|
+
args << "pass: #{pass_source(mappings)}"
|
|
1605
|
+
end
|
|
1606
|
+
"#{pad}#{method} #{args.join(', ')}"
|
|
1607
|
+
end
|
|
1608
|
+
|
|
1609
|
+
def pass_source(mappings)
|
|
1610
|
+
duplicate = mappings.map { _1.first.to_s }.tally.values.any? { _1 > 1 }
|
|
1611
|
+
rendered = mappings.map do |parameter, value|
|
|
1612
|
+
key = ruby(parameter)
|
|
1613
|
+
val = ruby_val(value)
|
|
1614
|
+
duplicate ? "[#{key}, #{val}]" : "#{key} => #{val}"
|
|
1615
|
+
end
|
|
1616
|
+
duplicate ? "[#{rendered.join(', ')}]" : "{ #{rendered.join(', ')} }"
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1619
|
+
def code_action_parameter_value(value)
|
|
1620
|
+
value ||= {}
|
|
1621
|
+
type = value["$Type"].to_s
|
|
1622
|
+
return value["Argument"] if type.include?("$Basic")
|
|
1623
|
+
|
|
1624
|
+
field, kind = case type
|
|
1625
|
+
when /EntityType/ then ["Entity", :entity]
|
|
1626
|
+
when /MicroflowJava/ then ["Microflow", :microflow]
|
|
1627
|
+
when /ImportMapping/ then ["ImportMapping", :import_mapping]
|
|
1628
|
+
when /ExportMapping/ then ["ExportMapping", :export_mapping]
|
|
1629
|
+
else
|
|
1630
|
+
[nil, :native]
|
|
1631
|
+
end
|
|
1632
|
+
{ kind: kind, value: field ? value[field] : value }
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
def translated_text(text_doc)
|
|
1636
|
+
return "" unless text_doc
|
|
1637
|
+
|
|
1638
|
+
translations = text_doc["Translations"] || text_doc["Items"]
|
|
1639
|
+
bson_items(translations).first&.fetch("Text", "") || ""
|
|
1640
|
+
end
|
|
1641
|
+
|
|
1642
|
+
def translated_items(text_doc)
|
|
1643
|
+
return [] unless text_doc
|
|
1644
|
+
|
|
1645
|
+
bson_items(text_doc["Translations"] || text_doc["Items"])
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
def member_specs(members_bson)
|
|
1649
|
+
bson_items(members_bson).filter_map do |m|
|
|
1650
|
+
attr = m["Attribute"].to_s
|
|
1651
|
+
association = m["Association"].to_s
|
|
1652
|
+
raw_value = m["Value"]
|
|
1653
|
+
val = raw_value.is_a?(Hash) ? raw_value["Value"].to_s : raw_value.to_s
|
|
1654
|
+
next if attr.empty? && association.empty?
|
|
1655
|
+
|
|
1656
|
+
{
|
|
1657
|
+
attribute: attr, association: association, value: val,
|
|
1658
|
+
operation: m.fetch("Type", "Set")
|
|
1659
|
+
}
|
|
1660
|
+
end
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
def members_dsl(members)
|
|
1664
|
+
members.map { "#{symbol(_1[:attribute])} => #{ruby_val(_1[:value])}" }.join(", ")
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
def member_block(command, members, indent)
|
|
1668
|
+
pad = " " * (indent + 2)
|
|
1669
|
+
lines = ["#{command} do"]
|
|
1670
|
+
members.each do |member|
|
|
1671
|
+
if member[:association].to_s.empty?
|
|
1672
|
+
lines << "#{pad}set #{symbol(member[:attribute])}, to: #{ruby_val(member[:value])}"
|
|
1673
|
+
else
|
|
1674
|
+
operation = underscore(member[:operation])
|
|
1675
|
+
operation_a = operation == "set" ? "" : ", operation: :#{operation}"
|
|
1676
|
+
lines << "#{pad}set_association #{ruby(member[:association])}, " \
|
|
1677
|
+
"to: #{ruby_val(member[:value])}#{operation_a}"
|
|
1678
|
+
end
|
|
1679
|
+
end
|
|
1680
|
+
lines << "#{' ' * indent}end"
|
|
1681
|
+
lines.join("\n")
|
|
1682
|
+
end
|
|
1683
|
+
|
|
1684
|
+
def ruby_val(val)
|
|
1685
|
+
return "nil" if val.nil? || val.to_s.empty?
|
|
1686
|
+
return ruby(val) if val.is_a?(Hash) || val.is_a?(Array)
|
|
1687
|
+
s = val.to_s
|
|
1688
|
+
return s if s.match?(/\A-?\d+(?:\.\d+)?\z/) || %w[true false nil].include?(s)
|
|
1689
|
+
|
|
1690
|
+
ruby(s)
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
def bson_items(bson)
|
|
1694
|
+
IO::BsonCodec.parse_array(bson)[:items]
|
|
1695
|
+
rescue StandardError
|
|
1696
|
+
[]
|
|
1697
|
+
end
|
|
1698
|
+
|
|
1699
|
+
def ruby(value)
|
|
1700
|
+
value.inspect
|
|
1701
|
+
end
|
|
1702
|
+
|
|
1703
|
+
def architecture_module(name)
|
|
1704
|
+
@architecture&.fetch(:modules, [])&.find { _1[:name] == name }
|
|
1705
|
+
end
|
|
1706
|
+
|
|
1707
|
+
def symbol(value)
|
|
1708
|
+
value.to_s.to_sym.inspect
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
def write(path, contents)
|
|
1712
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
1713
|
+
File.write(path, contents)
|
|
1714
|
+
end
|
|
1715
|
+
end
|
|
1716
|
+
end
|