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,1636 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "base64"
|
|
6
|
+
|
|
7
|
+
module Mxrb
|
|
8
|
+
module Dsl
|
|
9
|
+
# Shared widget-building methods for PageBuilder and ContainerBuilder.
|
|
10
|
+
# Includers must implement private `_widget_list` returning the target array.
|
|
11
|
+
module WidgetDsl
|
|
12
|
+
def text_box(name, attribute: nil, caption: nil, &block)
|
|
13
|
+
_add_widget(:text_box, name, attribute: attribute, caption: caption, &block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def number_input(name, attribute: nil, caption: nil, &block)
|
|
17
|
+
_add_widget(:number_input, name, attribute: attribute, caption: caption, &block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def text_area(name, attribute: nil, caption: nil, lines: 5, &block)
|
|
21
|
+
_add_widget(
|
|
22
|
+
:text_area, name, attribute: attribute, caption: caption, lines: lines, &block
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def check_box(name, attribute: nil, caption: nil, &block)
|
|
27
|
+
_add_widget(:check_box, name, attribute: attribute, caption: caption, &block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def date_picker(name, attribute: nil, caption: nil, &block)
|
|
31
|
+
_add_widget(:date_picker, name, attribute: attribute, caption: caption, &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reference_selector(name, attribute: nil, caption: nil, display_attribute: nil, &block)
|
|
35
|
+
_add_widget(
|
|
36
|
+
:reference_selector, name, attribute: attribute, caption: caption,
|
|
37
|
+
display_attribute: display_attribute, &block
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def text(name, caption: nil, &block)
|
|
42
|
+
_add_widget(:text, name, caption: caption || name.to_s, &block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def data_grid(name, entity: nil, &block)
|
|
46
|
+
_add_widget(:data_grid, name, entity: entity, &block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tab_control(name, &block)
|
|
50
|
+
_add_widget(:tab_control, name, &block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def button(name, caption: nil, &block)
|
|
54
|
+
_add_widget(:button, name, caption: caption || name.to_s, &block)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def drop_down(name, attribute: nil, caption: nil)
|
|
58
|
+
_widget_list << {
|
|
59
|
+
type: :drop_down, name: name.to_s,
|
|
60
|
+
options: { attribute: attribute&.to_s, caption: caption }.compact,
|
|
61
|
+
events: []
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def snippet(name, from: nil)
|
|
66
|
+
_widget_list << {
|
|
67
|
+
type: :snippet, name: name.to_s,
|
|
68
|
+
options: { snippet: (from || name).to_s },
|
|
69
|
+
events: []
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def container(name, class_name: nil, &block)
|
|
74
|
+
cb = ContainerBuilder.new(name, class_name: class_name)
|
|
75
|
+
cb.instance_eval(&block) if block
|
|
76
|
+
_widget_list << cb.to_h
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def pluggable_widget(name, widget_id:, widget_name: nil, properties: {}, class_name: nil)
|
|
80
|
+
_widget_list << {
|
|
81
|
+
type: :pluggable_widget, name: name.to_s,
|
|
82
|
+
options: {
|
|
83
|
+
widget_id: widget_id.to_s, widget_name: (widget_name || name).to_s,
|
|
84
|
+
properties: properties, class: class_name
|
|
85
|
+
}.compact,
|
|
86
|
+
events: []
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def native_widget(name, type:, deep_structure:)
|
|
91
|
+
raise ArgumentError, "deep_structure requires a Hash" unless deep_structure.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
_widget_list << {
|
|
94
|
+
type: :native_widget, name: name.to_s,
|
|
95
|
+
options: { native_type: type.to_s, deep_structure: deep_structure }, events: []
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Exported native widget payloads can contain BSON identifiers at any
|
|
100
|
+
# nesting level. Keep the reconstruction helper on the shared widget DSL
|
|
101
|
+
# so pages, containers, and tab pages all evaluate the same export.
|
|
102
|
+
def bson_binary(base64, subtype: :generic)
|
|
103
|
+
BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def _add_widget(type, name, **options, &block)
|
|
109
|
+
builder = WidgetBuilder.new(type, name, **options)
|
|
110
|
+
builder.instance_eval(&block) if block
|
|
111
|
+
_widget_list << builder.to_h
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Builds sub-widgets inside a data_grid column sub-items or inside a container.
|
|
116
|
+
class WidgetBuilder
|
|
117
|
+
def initialize(type, name, **options)
|
|
118
|
+
@type = type
|
|
119
|
+
@name = name.to_s
|
|
120
|
+
@options = options
|
|
121
|
+
@events = []
|
|
122
|
+
@columns = []
|
|
123
|
+
@tabs = []
|
|
124
|
+
@search_bar = nil
|
|
125
|
+
@toolbar = nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def column(name, attribute: nil, caption: nil)
|
|
129
|
+
@columns << { name: name.to_s, attribute: attribute&.to_s, caption: caption }.compact
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def tab_page(name, caption: nil, &block)
|
|
133
|
+
tab = TabPageBuilder.new(name, caption: caption)
|
|
134
|
+
tab.instance_eval(&block) if block
|
|
135
|
+
@tabs << tab.to_h
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def search_bar(&block)
|
|
139
|
+
sb = SearchBarBuilder.new
|
|
140
|
+
sb.instance_eval(&block) if block
|
|
141
|
+
@search_bar = sb.to_h
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def toolbar(&block)
|
|
145
|
+
tb = ToolbarBuilder.new
|
|
146
|
+
tb.instance_eval(&block) if block
|
|
147
|
+
@toolbar = tb.to_h
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
%i[on_change on_click on_enter on_leave].each do |event|
|
|
151
|
+
define_method(event) do |microflow: nil, nanoflow: nil, action: nil|
|
|
152
|
+
choices = { microflow: microflow, nanoflow: nanoflow, action: action }.compact
|
|
153
|
+
raise ArgumentError, "#{event} requires exactly one handler" unless choices.size == 1
|
|
154
|
+
@events << { event: event, kind: choices.keys.first, handler: choices.values.first.to_s }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def to_h
|
|
159
|
+
options = @options.dup
|
|
160
|
+
options[:columns] = @columns unless @columns.empty?
|
|
161
|
+
options[:tabs] = @tabs unless @tabs.empty?
|
|
162
|
+
options[:search_bar] = @search_bar if @search_bar
|
|
163
|
+
options[:toolbar] = @toolbar if @toolbar
|
|
164
|
+
{ type: @type, name: @name, options: options, events: @events }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
class TabPageBuilder
|
|
169
|
+
include WidgetDsl
|
|
170
|
+
|
|
171
|
+
def initialize(name, caption: nil)
|
|
172
|
+
@name = name.to_s
|
|
173
|
+
@caption = caption || name.to_s
|
|
174
|
+
@widgets = []
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def to_h = { name: @name, caption: @caption, widgets: @widgets }
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def _widget_list = @widgets
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class SearchBarBuilder
|
|
185
|
+
def initialize
|
|
186
|
+
@fields = []
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def search_field(attribute, caption: nil)
|
|
190
|
+
@fields << { attribute: attribute.to_s, caption: caption }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def to_h
|
|
194
|
+
{ fields: @fields }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
class ToolbarBuilder
|
|
199
|
+
def initialize
|
|
200
|
+
@buttons = []
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def new_button(caption: "New")
|
|
204
|
+
@buttons << { type: :new, caption: caption }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def delete_button(caption: "Delete")
|
|
208
|
+
@buttons << { type: :delete, caption: caption }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def search_button(caption: "Search")
|
|
212
|
+
@buttons << { type: :search, caption: caption }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def export_button(caption: "Export")
|
|
216
|
+
@buttons << { type: :export, caption: caption }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def to_h
|
|
220
|
+
{ buttons: @buttons }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Builds nested widgets inside a container widget.
|
|
225
|
+
class ContainerBuilder
|
|
226
|
+
include WidgetDsl
|
|
227
|
+
|
|
228
|
+
def initialize(name, class_name: nil)
|
|
229
|
+
@name = name.to_s
|
|
230
|
+
@class_name = class_name&.to_s
|
|
231
|
+
@children = []
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def to_h
|
|
235
|
+
opts = {}
|
|
236
|
+
opts[:class] = @class_name if @class_name && !@class_name.empty?
|
|
237
|
+
{ type: :container, name: @name, options: opts, children: @children, events: [] }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def _widget_list = @children
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Entry point for the project-definition DSL.
|
|
246
|
+
# Used by Mxrb.define { ... }
|
|
247
|
+
module ConnectorDeclarations
|
|
248
|
+
def connector(protocol, version: nil, marketplace_id: nil)
|
|
249
|
+
connector_requests << Protocols.request(protocol, version:, marketplace_id:)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def connector_plans(adapter: nil)
|
|
253
|
+
connector_requests.map do |request|
|
|
254
|
+
Protocols.plan(
|
|
255
|
+
request.protocol, mendix_version: @mendix_version, version: request.version,
|
|
256
|
+
marketplace_id: request.marketplace_id, adapter:
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def validate_connector_requests!
|
|
262
|
+
return if connector_requests.empty?
|
|
263
|
+
|
|
264
|
+
raise MarketplaceError,
|
|
265
|
+
'connector declarations are preview-only; resolve and apply connector_plans ' \
|
|
266
|
+
'to an existing MPR with the official Marketplace adapter'
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def connector_requests = (@connector_requests ||= [])
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
class Builder
|
|
273
|
+
include ConnectorDeclarations
|
|
274
|
+
|
|
275
|
+
def initialize(path)
|
|
276
|
+
@path = path
|
|
277
|
+
@mendix_version = "10.18.0"
|
|
278
|
+
@modules = []
|
|
279
|
+
@security = nil
|
|
280
|
+
@navigation = nil
|
|
281
|
+
@design_system = nil
|
|
282
|
+
@native_units_path = nil
|
|
283
|
+
@native_unit_overrides = []
|
|
284
|
+
@project_assets = nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def mendix_version(v)
|
|
288
|
+
@mendix_version = v
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def module(name, &block)
|
|
292
|
+
mod = ModuleBuilder.new(name)
|
|
293
|
+
mod.instance_eval(&block) if block
|
|
294
|
+
@modules << mod
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def security(&block)
|
|
298
|
+
builder = SecurityBuilder.new
|
|
299
|
+
builder.instance_eval(&block) if block
|
|
300
|
+
@security = builder.to_h
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def navigation(&block)
|
|
304
|
+
builder = NavigationBuilder.new
|
|
305
|
+
builder.instance_eval(&block) if block
|
|
306
|
+
@navigation = builder.to_h
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def design_system(&block)
|
|
310
|
+
builder = DesignSystemBuilder.new
|
|
311
|
+
builder.instance_eval(&block) if block
|
|
312
|
+
@design_system = builder.to_h
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def native_units(path)
|
|
316
|
+
@native_units_path = path
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def project_assets(manifest, root:)
|
|
320
|
+
@project_assets = {
|
|
321
|
+
manifest: File.expand_path(manifest),
|
|
322
|
+
root: File.expand_path(root)
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def native_unit(unit_id, container_id:, containment:, deep_structure:, module_name: nil)
|
|
327
|
+
raise ArgumentError, "deep_structure requires a Hash" unless deep_structure.is_a?(Hash)
|
|
328
|
+
|
|
329
|
+
@native_unit_overrides << {
|
|
330
|
+
unit_id: unit_id.to_s,
|
|
331
|
+
container_id: container_id.to_s,
|
|
332
|
+
containment: containment.to_s,
|
|
333
|
+
module: module_name&.to_s,
|
|
334
|
+
doc: deep_structure
|
|
335
|
+
}
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def bson_binary(base64, subtype: :generic)
|
|
339
|
+
BSON::Binary.new(Base64.strict_decode64(base64), subtype)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def evaluate(path)
|
|
343
|
+
instance_eval(File.read(path), path, 1)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def evaluate_dir(dir)
|
|
347
|
+
return unless File.directory?(dir)
|
|
348
|
+
|
|
349
|
+
Dir[File.join(dir, '*.rb')].sort.each { |path| evaluate(path) }
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def build!
|
|
353
|
+
validate_connector_requests!
|
|
354
|
+
validate!
|
|
355
|
+
Writer.new(@path, definition).write!
|
|
356
|
+
self
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def graph
|
|
360
|
+
Architecture::Graph.new(definition)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def validate!
|
|
364
|
+
Architecture::Validator.new(graph).validate!
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def definition
|
|
368
|
+
{
|
|
369
|
+
version: @mendix_version,
|
|
370
|
+
modules: @modules.map(&:to_h),
|
|
371
|
+
security: @security,
|
|
372
|
+
navigation: @navigation,
|
|
373
|
+
design_system: @design_system,
|
|
374
|
+
connectors: connector_requests.map(&:to_h),
|
|
375
|
+
project_assets: @project_assets,
|
|
376
|
+
native_units_path: @native_units_path,
|
|
377
|
+
native_unit_overrides: @native_unit_overrides
|
|
378
|
+
}
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
class SecurityBuilder
|
|
383
|
+
def initialize
|
|
384
|
+
@user_roles = []
|
|
385
|
+
@security_level = nil
|
|
386
|
+
@admin_user_role = nil
|
|
387
|
+
@demo_users_enabled = nil
|
|
388
|
+
@guest_access_enabled = nil
|
|
389
|
+
@guest_user_role = nil
|
|
390
|
+
@sign_in_microflow = nil
|
|
391
|
+
@password_policy = nil
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Mendix stores the project security mode as an enum-like string such as
|
|
395
|
+
# "CheckNothing" or "CheckEverything". Keep the native value explicit so
|
|
396
|
+
# export/import does not silently weaken project security.
|
|
397
|
+
def security_level(value)
|
|
398
|
+
@security_level = value.to_s
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def user_role(name, module_roles: [], admin: false)
|
|
402
|
+
@user_roles << {
|
|
403
|
+
name: name.to_s,
|
|
404
|
+
module_roles: Array(module_roles).map(&:to_s),
|
|
405
|
+
admin: admin
|
|
406
|
+
}
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def admin_user_role(name)
|
|
410
|
+
@admin_user_role = name.to_s
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def demo_users(enabled = true)
|
|
414
|
+
@demo_users_enabled = enabled == true
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def guest_access(enabled = true, role: nil)
|
|
418
|
+
@guest_access_enabled = enabled == true
|
|
419
|
+
@guest_user_role = role&.to_s
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def sign_in_microflow(name)
|
|
423
|
+
@sign_in_microflow = name&.to_s
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def password_policy(**options)
|
|
427
|
+
@password_policy = options.transform_keys(&:to_sym)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def to_h
|
|
431
|
+
{
|
|
432
|
+
user_roles: @user_roles,
|
|
433
|
+
security_level: @security_level,
|
|
434
|
+
admin_user_role: @admin_user_role,
|
|
435
|
+
demo_users_enabled: @demo_users_enabled,
|
|
436
|
+
guest_access_enabled: @guest_access_enabled,
|
|
437
|
+
guest_user_role: @guest_user_role,
|
|
438
|
+
sign_in_microflow: @sign_in_microflow,
|
|
439
|
+
password_policy: @password_policy
|
|
440
|
+
}
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
class NavigationBuilder
|
|
445
|
+
def initialize
|
|
446
|
+
@profiles = []
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def profile(name, home_page: nil, home_microflow: nil, sign_in_page: nil,
|
|
450
|
+
menu: nil, role_homes: {}, offline: false, kind: nil,
|
|
451
|
+
app_title: nil, app_icon: nil, &block)
|
|
452
|
+
profile = NavigationProfileBuilder.new(
|
|
453
|
+
name, home_page:, home_microflow:, sign_in_page:, menu:, role_homes:,
|
|
454
|
+
offline:, kind:, app_title:, app_icon:
|
|
455
|
+
)
|
|
456
|
+
profile.instance_eval(&block) if block
|
|
457
|
+
@profiles << profile.to_h
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def to_h
|
|
461
|
+
{ profiles: @profiles }
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
class NavigationProfileBuilder
|
|
466
|
+
def initialize(name, **options)
|
|
467
|
+
@name = name.to_s
|
|
468
|
+
@home_page = optional_string(options, :home_page)
|
|
469
|
+
@home_microflow = optional_string(options, :home_microflow)
|
|
470
|
+
@sign_in_page = optional_string(options, :sign_in_page)
|
|
471
|
+
@menu = optional_string(options, :menu)
|
|
472
|
+
@offline = options[:offline] == true
|
|
473
|
+
@kind = optional_string(options, :kind)
|
|
474
|
+
@app_icon = optional_string(options, :app_icon)
|
|
475
|
+
@app_title = normalize_translations(options[:app_title])
|
|
476
|
+
@role_homes = options.fetch(:role_homes, {}).to_h.transform_keys(&:to_s)
|
|
477
|
+
.transform_values(&:to_s)
|
|
478
|
+
@role_home_details = []
|
|
479
|
+
@items = []
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def title(locale, text)
|
|
483
|
+
@app_title[locale.to_s] = text.to_s
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def home_for(role, page: nil, microflow: nil)
|
|
487
|
+
@role_home_details << {
|
|
488
|
+
role: role.to_s, page: page&.to_s, microflow: microflow&.to_s
|
|
489
|
+
}.compact
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def item(caption, page: nil, microflow: nil, icon: nil, translations: {}, &block)
|
|
493
|
+
builder = NavigationMenuItemBuilder.new(
|
|
494
|
+
caption, page:, microflow:, icon:, translations:
|
|
495
|
+
)
|
|
496
|
+
builder.instance_eval(&block) if block
|
|
497
|
+
@items << builder.to_h
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def evaluate(path)
|
|
501
|
+
instance_eval(File.read(path), path, 1)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def evaluate_dir(dir)
|
|
505
|
+
return unless File.directory?(dir)
|
|
506
|
+
|
|
507
|
+
Dir[File.join(dir, '*.rb')].sort.each { |path| evaluate(path) }
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def to_h
|
|
511
|
+
{
|
|
512
|
+
name: @name, home_page: @home_page, home_microflow: @home_microflow,
|
|
513
|
+
sign_in_page: @sign_in_page, menu: @menu, role_homes: @role_homes,
|
|
514
|
+
role_home_details: @role_home_details, offline: @offline, kind: @kind,
|
|
515
|
+
app_title: @app_title, app_icon: @app_icon, items: @items
|
|
516
|
+
}
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
private
|
|
520
|
+
|
|
521
|
+
def optional_string(options, key)
|
|
522
|
+
options[key]&.to_s
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def normalize_translations(value)
|
|
526
|
+
return {} if value.nil?
|
|
527
|
+
return value.to_h.transform_keys(&:to_s).transform_values(&:to_s) if value.respond_to?(:to_h)
|
|
528
|
+
|
|
529
|
+
{ "en_US" => value.to_s }
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
class NavigationMenuItemBuilder
|
|
534
|
+
def initialize(caption, page: nil, microflow: nil, icon: nil, translations: {})
|
|
535
|
+
@caption = normalize_caption(caption)
|
|
536
|
+
@caption.merge!(translations.to_h.transform_keys(&:to_s).transform_values(&:to_s))
|
|
537
|
+
@page = page&.to_s
|
|
538
|
+
@microflow = microflow&.to_s
|
|
539
|
+
@icon = icon
|
|
540
|
+
@items = []
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def item(caption, page: nil, microflow: nil, icon: nil, translations: {}, &block)
|
|
544
|
+
child = self.class.new(caption, page:, microflow:, icon:, translations:)
|
|
545
|
+
child.instance_eval(&block) if block
|
|
546
|
+
@items << child.to_h
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def to_h
|
|
550
|
+
{
|
|
551
|
+
caption: @caption, page: @page, microflow: @microflow,
|
|
552
|
+
icon: @icon, items: @items
|
|
553
|
+
}
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
private
|
|
557
|
+
|
|
558
|
+
def normalize_caption(caption)
|
|
559
|
+
return { "en_US" => caption.to_s } unless caption.respond_to?(:to_h)
|
|
560
|
+
|
|
561
|
+
caption.to_h.transform_keys(&:to_s).transform_values(&:to_s)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
class DesignSystemBuilder
|
|
566
|
+
TOKEN_KINDS = %i[color spacing radius typography breakpoint].freeze
|
|
567
|
+
|
|
568
|
+
def initialize
|
|
569
|
+
@tokens = []
|
|
570
|
+
@themes = []
|
|
571
|
+
@layouts = []
|
|
572
|
+
@components = []
|
|
573
|
+
@accessibility = []
|
|
574
|
+
@contrast_pairs = []
|
|
575
|
+
@forbid_literal_colors = false
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
TOKEN_KINDS.each do |kind|
|
|
579
|
+
define_method(kind) do |name, value: nil|
|
|
580
|
+
@tokens << { kind:, name: name.to_s, value: value&.to_s }
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def layout(name)
|
|
585
|
+
@layouts << name.to_s
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def component(name)
|
|
589
|
+
@components << name.to_s
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def accessibility(rule)
|
|
593
|
+
@accessibility << rule.to_s
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def theme(name, inherits: nil, &block)
|
|
597
|
+
builder = DesignThemeBuilder.new(name, inherits:)
|
|
598
|
+
builder.instance_eval(&block) if block
|
|
599
|
+
@themes << builder.to_h
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def contrast(foreground:, background:, level: :aa)
|
|
603
|
+
@contrast_pairs << {
|
|
604
|
+
foreground: foreground.to_s, background: background.to_s, level: level.to_s
|
|
605
|
+
}
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def forbid_literal_colors(value = true)
|
|
609
|
+
@forbid_literal_colors = value == true
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def to_h
|
|
613
|
+
{
|
|
614
|
+
tokens: @tokens, layouts: @layouts,
|
|
615
|
+
components: @components, accessibility: @accessibility,
|
|
616
|
+
themes: @themes, contrast_pairs: @contrast_pairs,
|
|
617
|
+
forbid_literal_colors: @forbid_literal_colors
|
|
618
|
+
}
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
class DesignThemeBuilder
|
|
623
|
+
def initialize(name, inherits: nil)
|
|
624
|
+
@name = name.to_s
|
|
625
|
+
@inherits = inherits&.to_s
|
|
626
|
+
@tokens = []
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
DesignSystemBuilder::TOKEN_KINDS.each do |kind|
|
|
630
|
+
define_method(kind) do |name, value: nil|
|
|
631
|
+
@tokens << { kind:, name: name.to_s, value: value&.to_s }
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def to_h = { name: @name, inherits: @inherits, tokens: @tokens }
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
class EnumerationBuilder
|
|
639
|
+
attr_reader :name
|
|
640
|
+
|
|
641
|
+
def initialize(name)
|
|
642
|
+
@name = name.to_s
|
|
643
|
+
@values = []
|
|
644
|
+
@doc = ""
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def value(name, caption: nil)
|
|
648
|
+
@values << { name: name.to_s, caption: caption&.to_s }
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def documentation(d) = (@doc = d)
|
|
652
|
+
|
|
653
|
+
def to_h
|
|
654
|
+
{ name: @name, values: @values, documentation: @doc }
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
class ConstantBuilder
|
|
659
|
+
CONSTANT_TYPES = {
|
|
660
|
+
string: "DataTypes$StringType", integer: "DataTypes$IntegerType",
|
|
661
|
+
boolean: "DataTypes$BooleanType", decimal: "DataTypes$DecimalType",
|
|
662
|
+
datetime: "DataTypes$DateTimeType"
|
|
663
|
+
}.freeze
|
|
664
|
+
|
|
665
|
+
attr_reader :name
|
|
666
|
+
|
|
667
|
+
def initialize(name, type:, value: nil)
|
|
668
|
+
@name = name.to_s
|
|
669
|
+
@type = type.to_sym
|
|
670
|
+
@value = value
|
|
671
|
+
@doc = ""
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def documentation(d) = (@doc = d)
|
|
675
|
+
|
|
676
|
+
def to_h
|
|
677
|
+
{ name: @name, type: @type, value: @value, documentation: @doc }
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
class ScheduledEventBuilder
|
|
682
|
+
INTERVAL_UNITS = {
|
|
683
|
+
milliseconds: "Millisecond", seconds: "Second", minutes: "Minute",
|
|
684
|
+
hours: "Hour", days: "Day", weeks: "Week", months: "Month", years: "Year"
|
|
685
|
+
}.freeze
|
|
686
|
+
|
|
687
|
+
attr_reader :name
|
|
688
|
+
|
|
689
|
+
def initialize(name, microflow:, interval: 1, unit: :days, enabled: true)
|
|
690
|
+
@name = name.to_s
|
|
691
|
+
@microflow = microflow.to_s
|
|
692
|
+
@interval = interval.to_i
|
|
693
|
+
@unit = unit.to_sym
|
|
694
|
+
@enabled = enabled
|
|
695
|
+
@doc = ""
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def documentation(d) = (@doc = d)
|
|
699
|
+
|
|
700
|
+
def to_h
|
|
701
|
+
{
|
|
702
|
+
name: @name, microflow: @microflow, interval: @interval,
|
|
703
|
+
unit: @unit, enabled: @enabled, documentation: @doc
|
|
704
|
+
}
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
class ModuleBuilder # rubocop:disable Metrics/ClassLength
|
|
709
|
+
attr_reader :name, :entities, :pages, :microflows, :nanoflows, :repositories,
|
|
710
|
+
:associations, :menus, :module_roles, :enumerations, :constants,
|
|
711
|
+
:scheduled_events, :native_documents
|
|
712
|
+
|
|
713
|
+
def initialize(name)
|
|
714
|
+
@name = name.to_s
|
|
715
|
+
@entities = []
|
|
716
|
+
@pages = []
|
|
717
|
+
@microflows = []
|
|
718
|
+
@nanoflows = []
|
|
719
|
+
@repositories = []
|
|
720
|
+
@associations = []
|
|
721
|
+
@menus = []
|
|
722
|
+
@module_roles = []
|
|
723
|
+
@enumerations = []
|
|
724
|
+
@constants = []
|
|
725
|
+
@scheduled_events = []
|
|
726
|
+
@native_documents = []
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def entity(name, &block)
|
|
730
|
+
eb = EntityBuilder.new(name)
|
|
731
|
+
eb.instance_eval(&block) if block
|
|
732
|
+
@entities << eb.to_h
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def page(name, &block)
|
|
736
|
+
pb = PageBuilder.new(name)
|
|
737
|
+
pb.instance_eval(&block) if block
|
|
738
|
+
@pages << pb.to_h
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def menu(name, &block)
|
|
742
|
+
mb = MenuBuilder.new(name)
|
|
743
|
+
mb.instance_eval(&block) if block
|
|
744
|
+
@menus << mb.to_h
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def module_role(name, description: "")
|
|
748
|
+
@module_roles << { name: name.to_s, description: description.to_s }
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
def microflow(name, kind: :use_case, public: false, &block)
|
|
752
|
+
fb = FlowBuilder.new(name, runtime: :server, kind: kind, public: public)
|
|
753
|
+
fb.instance_eval(&block) if block
|
|
754
|
+
@microflows << fb.to_h
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def nanoflow(name, public: false, &block)
|
|
758
|
+
fb = FlowBuilder.new(name, runtime: :client, kind: :client_action, public: public)
|
|
759
|
+
fb.instance_eval(&block) if block
|
|
760
|
+
@nanoflows << fb.to_h
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def query(name, public: false, &block)
|
|
764
|
+
microflow(name, kind: :query, public: public, &block)
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def repository(name, implementation: nil, public: false, documentation: "")
|
|
768
|
+
@repositories << {
|
|
769
|
+
name: name.to_s, implementation: implementation&.to_s,
|
|
770
|
+
public: public, documentation: documentation.to_s
|
|
771
|
+
}
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def enumeration(name, &block)
|
|
775
|
+
eb = EnumerationBuilder.new(name)
|
|
776
|
+
eb.instance_eval(&block) if block
|
|
777
|
+
@enumerations << eb.to_h
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def constant(name, type:, value: nil, &block)
|
|
781
|
+
cb = ConstantBuilder.new(name, type: type, value: value)
|
|
782
|
+
cb.instance_eval(&block) if block
|
|
783
|
+
@constants << cb.to_h
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def scheduled_event(name, microflow:, interval: 1, unit: :days, enabled: true, &block)
|
|
787
|
+
sb = ScheduledEventBuilder.new(
|
|
788
|
+
name, microflow: microflow, interval: interval, unit: unit, enabled: enabled
|
|
789
|
+
)
|
|
790
|
+
sb.instance_eval(&block) if block
|
|
791
|
+
@scheduled_events << sb.to_h
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def native_document(name, type:, deep_structure:, containment: 'Documents')
|
|
795
|
+
raise ArgumentError, 'deep_structure requires a Hash' unless deep_structure.is_a?(Hash)
|
|
796
|
+
|
|
797
|
+
@native_documents << {
|
|
798
|
+
name: name.to_s, type: type.to_s, containment: containment.to_s,
|
|
799
|
+
doc: { '$Type' => type.to_s, 'Name' => name.to_s }.merge(deep_structure)
|
|
800
|
+
}
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Native responsive application shell. It deliberately uses only Mendix
|
|
804
|
+
# core widgets, so generated projects do not depend on Atlas Core merely
|
|
805
|
+
# to display their navigation.
|
|
806
|
+
def layout(name = :ApplicationLayout, title: nil, navigation: :Responsive)
|
|
807
|
+
appearance = lambda do |class_name = ''|
|
|
808
|
+
{
|
|
809
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$Appearance',
|
|
810
|
+
'Class' => class_name, 'DesignProperties' => IO::BsonCodec.build_array([]),
|
|
811
|
+
'DynamicClasses' => '', 'Style' => ''
|
|
812
|
+
}
|
|
813
|
+
end
|
|
814
|
+
placeholder = {
|
|
815
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$Placeholder',
|
|
816
|
+
'Appearance' => appearance.call, 'Name' => 'Main', 'TabIndex' => 0
|
|
817
|
+
}
|
|
818
|
+
text = lambda do |value|
|
|
819
|
+
{
|
|
820
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Texts$Text',
|
|
821
|
+
'Items' => IO::BsonCodec.build_array([
|
|
822
|
+
{
|
|
823
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Texts$Translation',
|
|
824
|
+
'LanguageCode' => 'en_US', 'Text' => value.to_s
|
|
825
|
+
}
|
|
826
|
+
])
|
|
827
|
+
}
|
|
828
|
+
end
|
|
829
|
+
client_template = lambda do |value|
|
|
830
|
+
{
|
|
831
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$ClientTemplate',
|
|
832
|
+
'Fallback' => text.call(''),
|
|
833
|
+
'Parameters' => IO::BsonCodec.build_array([], marker: 2),
|
|
834
|
+
'Template' => text.call(value)
|
|
835
|
+
}
|
|
836
|
+
end
|
|
837
|
+
no_action = {
|
|
838
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$NoAction',
|
|
839
|
+
'DisabledDuringExecution' => true
|
|
840
|
+
}
|
|
841
|
+
navigation_tree = {
|
|
842
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$NavigationTree',
|
|
843
|
+
'Appearance' => appearance.call('mxrb-navigation'),
|
|
844
|
+
'MenuSource' => {
|
|
845
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$NavigationSource',
|
|
846
|
+
'NavigationProfile' => navigation.to_s
|
|
847
|
+
},
|
|
848
|
+
'Name' => 'navigationTree1', 'TabIndex' => 0
|
|
849
|
+
}
|
|
850
|
+
left = {
|
|
851
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$ScrollContainerRegion',
|
|
852
|
+
'Appearance' => appearance.call('region-sidebar'),
|
|
853
|
+
'Size' => 264, 'SizeMode' => 'Pixels',
|
|
854
|
+
'ToggleMode' => 'ShrinkContentInitiallyClosed',
|
|
855
|
+
'Widgets' => IO::BsonCodec.build_array([navigation_tree], marker: 2)
|
|
856
|
+
}
|
|
857
|
+
sidebar_toggle = {
|
|
858
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$SidebarToggleButton',
|
|
859
|
+
'Appearance' => appearance.call('mxrb-sidebar-toggle'),
|
|
860
|
+
'ButtonStyle' => 'Primary', 'CaptionTemplate' => client_template.call('Menu'),
|
|
861
|
+
'ConditionalVisibilitySettings' => nil, 'Icon' => nil,
|
|
862
|
+
'Name' => 'sidebarToggle1', 'RenderType' => 'Button', 'TabIndex' => 0,
|
|
863
|
+
'Tooltip' => text.call('Toggle navigation')
|
|
864
|
+
}
|
|
865
|
+
brand = {
|
|
866
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$DynamicText',
|
|
867
|
+
'Appearance' => appearance.call('mxrb-brand'), 'Class' => '',
|
|
868
|
+
'ConditionalVisibilitySettings' => nil,
|
|
869
|
+
'Content' => client_template.call(title || name.to_s),
|
|
870
|
+
'Name' => 'applicationTitle', 'NativeAccessibilitySettings' => nil,
|
|
871
|
+
'NativeTextStyle' => 'Text', 'RenderMode' => 'Text', 'Style' => '',
|
|
872
|
+
'TabIndex' => 0
|
|
873
|
+
}
|
|
874
|
+
topbar_content = {
|
|
875
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$DivContainer',
|
|
876
|
+
'Appearance' => appearance.call('mxrb-topbar'),
|
|
877
|
+
'ConditionalVisibilitySettings' => nil, 'Name' => 'topbarContent',
|
|
878
|
+
'OnClickAction' => no_action, 'RenderMode' => 'Div',
|
|
879
|
+
'ScreenReaderHidden' => false, 'TabIndex' => 0,
|
|
880
|
+
'Widgets' => IO::BsonCodec.build_array([sidebar_toggle, brand], marker: 2)
|
|
881
|
+
}
|
|
882
|
+
top = {
|
|
883
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$ScrollContainerRegion',
|
|
884
|
+
'Appearance' => appearance.call('region-topbar'),
|
|
885
|
+
'Size' => 72, 'SizeMode' => 'Pixels', 'ToggleMode' => 'None',
|
|
886
|
+
'Widgets' => IO::BsonCodec.build_array([topbar_content], marker: 2)
|
|
887
|
+
}
|
|
888
|
+
center = {
|
|
889
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$ScrollContainerRegion',
|
|
890
|
+
'Appearance' => appearance.call('region-content'),
|
|
891
|
+
'Size' => 200, 'SizeMode' => 'Auto',
|
|
892
|
+
'ToggleMode' => 'None',
|
|
893
|
+
'Widgets' => IO::BsonCodec.build_array([placeholder], marker: 2)
|
|
894
|
+
}
|
|
895
|
+
container = {
|
|
896
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$ScrollContainer',
|
|
897
|
+
'Alignment' => 'Center', 'Bottom' => nil, 'CenterRegion' => center,
|
|
898
|
+
'Appearance' => appearance.call, 'LayoutMode' => 'Headline', 'Left' => left,
|
|
899
|
+
'Name' => 'scrollContainer1', 'Right' => nil, 'ScrollBehavior' => 'PerRegion',
|
|
900
|
+
'NativeHideScrollbars' => false, 'TabIndex' => 0, 'Top' => top,
|
|
901
|
+
'Width' => 960, 'WidthMode' => 'Auto'
|
|
902
|
+
}
|
|
903
|
+
content = {
|
|
904
|
+
'$ID' => SecureRandom.uuid, '$Type' => 'Forms$WebLayoutContent',
|
|
905
|
+
'LayoutCall' => nil, 'LayoutType' => 'Responsive',
|
|
906
|
+
'Widgets' => IO::BsonCodec.build_array([container], marker: 2)
|
|
907
|
+
}
|
|
908
|
+
native_document(
|
|
909
|
+
name, type: 'Forms$Layout', deep_structure: {
|
|
910
|
+
'Appearance' => appearance.call('mxrb-application-shell'), 'CanvasHeight' => 600,
|
|
911
|
+
'CanvasWidth' => 800, 'Content' => content, 'Documentation' => '',
|
|
912
|
+
'Excluded' => false, 'ExportLevel' => 'Hidden'
|
|
913
|
+
}
|
|
914
|
+
)
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
def evaluate(path)
|
|
918
|
+
instance_eval(File.read(path), path, 1)
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def evaluate_dir(dir)
|
|
922
|
+
return unless File.directory?(dir)
|
|
923
|
+
|
|
924
|
+
Dir[File.join(dir, '*.rb')].sort.each { |path| evaluate(path) }
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
def to_h
|
|
928
|
+
{
|
|
929
|
+
name: @name, entities: @entities, pages: @pages,
|
|
930
|
+
microflows: @microflows, nanoflows: @nanoflows,
|
|
931
|
+
repositories: @repositories, menus: @menus, module_roles: @module_roles,
|
|
932
|
+
enumerations: @enumerations, constants: @constants,
|
|
933
|
+
scheduled_events: @scheduled_events, native_documents: @native_documents
|
|
934
|
+
}
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
class MenuBuilder
|
|
939
|
+
def initialize(name)
|
|
940
|
+
@name = name.to_s
|
|
941
|
+
@items = []
|
|
942
|
+
@deep_structure = nil
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def deep_structure(value)
|
|
946
|
+
raise ArgumentError, "deep_structure requires a Hash" unless value.is_a?(Hash)
|
|
947
|
+
|
|
948
|
+
@deep_structure = value
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def bson_binary(base64, subtype: :generic)
|
|
952
|
+
BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
def item(caption, page: nil, &block)
|
|
956
|
+
child = self.class.new(caption)
|
|
957
|
+
child.instance_eval(&block) if block
|
|
958
|
+
@items << { caption: caption.to_s, page: page&.to_s, items: child.items }.compact
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
def items = @items
|
|
962
|
+
|
|
963
|
+
def to_h
|
|
964
|
+
{ name: @name, items: @items, deep_structure: @deep_structure }
|
|
965
|
+
end
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
class EntityBuilder # rubocop:disable Metrics/ClassLength
|
|
969
|
+
ATTR_TYPES = %i[string integer long float decimal boolean datetime autonumber hashstring binary enum].freeze
|
|
970
|
+
ASSOCIATION_TYPES = %i[Reference ReferenceSet].freeze
|
|
971
|
+
ASSOCIATION_OWNERS = %i[Default Both].freeze
|
|
972
|
+
ASSOCIATION_STORAGE_FORMATS = %i[Column Table].freeze
|
|
973
|
+
CARDINALITIES = {
|
|
974
|
+
many_to_one: %i[Reference Default],
|
|
975
|
+
one_to_one: %i[Reference Both],
|
|
976
|
+
many_to_many: %i[ReferenceSet Default]
|
|
977
|
+
}.freeze
|
|
978
|
+
|
|
979
|
+
attr_reader :name
|
|
980
|
+
|
|
981
|
+
def initialize(name)
|
|
982
|
+
@name = name.to_s
|
|
983
|
+
@attributes = []
|
|
984
|
+
@persistable = true
|
|
985
|
+
@doc = ""
|
|
986
|
+
@associations = []
|
|
987
|
+
@lifecycle = nil
|
|
988
|
+
@access_rules = nil
|
|
989
|
+
@generalization = nil
|
|
990
|
+
@system_members = nil
|
|
991
|
+
@indexes = nil
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
ATTR_TYPES.each do |type|
|
|
995
|
+
define_method(type) do |attr_name, **opts|
|
|
996
|
+
@attributes << { name: attr_name.to_s, type: type, **opts }
|
|
997
|
+
end
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
def non_persistent! = (@persistable = false)
|
|
1001
|
+
def documentation(d) = (@doc = d)
|
|
1002
|
+
|
|
1003
|
+
def generalizes(entity)
|
|
1004
|
+
@generalization = entity.to_s
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
def system_members(owner: false, created_date: false, changed_date: false, changed_by: false)
|
|
1008
|
+
@system_members = {
|
|
1009
|
+
owner: owner, created_date: created_date,
|
|
1010
|
+
changed_date: changed_date, changed_by: changed_by
|
|
1011
|
+
}
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
def index(*attributes, include_offline: false, ascending: true)
|
|
1015
|
+
members = attributes.flatten.map(&:to_s)
|
|
1016
|
+
raise ArgumentError, 'index requires at least one attribute' if members.empty?
|
|
1017
|
+
|
|
1018
|
+
@indexes ||= []
|
|
1019
|
+
directions = Array(ascending)
|
|
1020
|
+
if directions.size != 1 && directions.size != members.size
|
|
1021
|
+
raise ArgumentError, 'ascending must be one boolean or one value per indexed attribute'
|
|
1022
|
+
end
|
|
1023
|
+
directions *= members.size if directions.size == 1
|
|
1024
|
+
@indexes << {
|
|
1025
|
+
attributes: members, ascending: directions.map { _1 == true },
|
|
1026
|
+
include_offline: include_offline == true
|
|
1027
|
+
}
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
def association(target, type: :Reference, owner: :Default, name: nil, cardinality: nil,
|
|
1031
|
+
documentation: '', parent_delete: :NoAction, child_delete: :NoAction,
|
|
1032
|
+
storage_format: nil)
|
|
1033
|
+
if cardinality
|
|
1034
|
+
type, owner = CARDINALITIES.fetch(cardinality.to_sym) do
|
|
1035
|
+
raise ArgumentError, 'cardinality must be :many_to_one, :one_to_one, or :many_to_many'
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
type = type.to_sym
|
|
1039
|
+
owner = owner.to_sym
|
|
1040
|
+
unless ASSOCIATION_TYPES.include?(type)
|
|
1041
|
+
raise ArgumentError, "association type must be Reference or ReferenceSet"
|
|
1042
|
+
end
|
|
1043
|
+
raise ArgumentError, "association owner must be Default or Both" unless ASSOCIATION_OWNERS.include?(owner)
|
|
1044
|
+
storage_format = normalize_association_storage_format(storage_format)
|
|
1045
|
+
|
|
1046
|
+
@associations << {
|
|
1047
|
+
name: (name || "#{@name}_#{target}").to_s,
|
|
1048
|
+
target: target.to_s,
|
|
1049
|
+
type: type,
|
|
1050
|
+
owner: owner,
|
|
1051
|
+
documentation: documentation.to_s,
|
|
1052
|
+
parent_delete: parent_delete.to_sym,
|
|
1053
|
+
child_delete: child_delete.to_sym,
|
|
1054
|
+
storage_format: storage_format
|
|
1055
|
+
}
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
%i[before_commit after_commit before_delete after_delete].each do |event|
|
|
1059
|
+
define_method(event) do |microflow:|
|
|
1060
|
+
@lifecycle ||= []
|
|
1061
|
+
@lifecycle << { event: event, handler: microflow.to_s }
|
|
1062
|
+
end
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
# access_rule "Module.Role", create: true, delete: false, read: :all, write: [:Name]
|
|
1066
|
+
def access_rule(*roles, create: false, delete: false, read: :none, write: :none, xpath: "",
|
|
1067
|
+
default_rights: nil, members: nil)
|
|
1068
|
+
@access_rules ||= []
|
|
1069
|
+
@access_rules << {
|
|
1070
|
+
roles: roles.map(&:to_s),
|
|
1071
|
+
create: create,
|
|
1072
|
+
delete: delete,
|
|
1073
|
+
read: normalize_access(read),
|
|
1074
|
+
write: normalize_access(write),
|
|
1075
|
+
xpath: xpath.to_s,
|
|
1076
|
+
default_rights: default_rights&.to_s,
|
|
1077
|
+
members: members&.map { _1.transform_keys(&:to_sym) }
|
|
1078
|
+
}
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
def to_h
|
|
1082
|
+
{
|
|
1083
|
+
name: @name, persistable: @persistable, documentation: @doc,
|
|
1084
|
+
attributes: @attributes, associations: @associations, lifecycle: @lifecycle,
|
|
1085
|
+
access_rules: @access_rules, generalization: @generalization,
|
|
1086
|
+
system_members: @system_members, indexes: @indexes
|
|
1087
|
+
}
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
private
|
|
1091
|
+
|
|
1092
|
+
def normalize_association_storage_format(value)
|
|
1093
|
+
return if value.nil?
|
|
1094
|
+
|
|
1095
|
+
format = value.to_sym
|
|
1096
|
+
return format if ASSOCIATION_STORAGE_FORMATS.include?(format)
|
|
1097
|
+
|
|
1098
|
+
raise ArgumentError, "association storage format must be Column or Table"
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
def normalize_access(value)
|
|
1102
|
+
case value
|
|
1103
|
+
when :all, :none then value
|
|
1104
|
+
when Array then value.map(&:to_s)
|
|
1105
|
+
else value.to_sym
|
|
1106
|
+
end
|
|
1107
|
+
end
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
class PageBuilder
|
|
1111
|
+
include WidgetDsl
|
|
1112
|
+
|
|
1113
|
+
def initialize(name)
|
|
1114
|
+
@name = name.to_s
|
|
1115
|
+
@layout = "Atlas_Default"
|
|
1116
|
+
@title = name.to_s
|
|
1117
|
+
@popup = false
|
|
1118
|
+
@data_source = nil
|
|
1119
|
+
@events = []
|
|
1120
|
+
@widgets = []
|
|
1121
|
+
@allowed_roles = nil
|
|
1122
|
+
@deep_structure = nil
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def layout(l) = (@layout = l)
|
|
1126
|
+
def title(t) = (@title = t)
|
|
1127
|
+
def popup! = (@popup = true)
|
|
1128
|
+
|
|
1129
|
+
# Full, editable Mendix page payload for structures without a concise
|
|
1130
|
+
# typed DSL yet (layout grids, custom widgets, list views, etc.).
|
|
1131
|
+
def deep_structure(value)
|
|
1132
|
+
raise ArgumentError, "deep_structure requires a Hash" unless value.is_a?(Hash)
|
|
1133
|
+
|
|
1134
|
+
@deep_structure = value
|
|
1135
|
+
end
|
|
1136
|
+
|
|
1137
|
+
def bson_binary(base64, subtype: :generic)
|
|
1138
|
+
BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
def allowed_roles(*roles)
|
|
1142
|
+
@allowed_roles = roles.map(&:to_s)
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def data_source(query: nil, microflow: nil, nanoflow: nil)
|
|
1146
|
+
choices = { microflow: microflow || query, nanoflow: nanoflow }.compact
|
|
1147
|
+
raise ArgumentError, "data_source requires exactly one target" unless choices.size == 1
|
|
1148
|
+
@data_source = { kind: choices.keys.first, name: choices.values.first.to_s }
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
%i[on_change on_click on_submit on_load].each do |event|
|
|
1152
|
+
define_method(event) do |target: nil, microflow: nil, nanoflow: nil, action: nil|
|
|
1153
|
+
choices = { microflow: microflow, nanoflow: nanoflow, action: action }.compact
|
|
1154
|
+
raise ArgumentError, "#{event} requires exactly one handler" unless choices.size == 1
|
|
1155
|
+
@events << {
|
|
1156
|
+
event: event, target: target&.to_s,
|
|
1157
|
+
kind: choices.keys.first, handler: choices.values.first.to_s
|
|
1158
|
+
}
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
def to_h
|
|
1163
|
+
{
|
|
1164
|
+
name: @name, layout: @layout, title: @title, popup: @popup,
|
|
1165
|
+
data_source: @data_source, events: @events, widgets: @widgets,
|
|
1166
|
+
allowed_roles: @allowed_roles, deep_structure: @deep_structure
|
|
1167
|
+
}
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
private
|
|
1171
|
+
|
|
1172
|
+
def _widget_list = @widgets
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
# Shared activity DSL mixed into FlowBuilder, BranchBuilder, LoopBuilder, RescueBuilder
|
|
1176
|
+
module FlowBodyDsl
|
|
1177
|
+
def create_object(entity, as:, set: {}, commit: false, with_events: true,
|
|
1178
|
+
refresh: false, &block)
|
|
1179
|
+
_acts << { type: :create_object, entity: entity.to_s, variable: as.to_s,
|
|
1180
|
+
members: _build_members(set, &block), commit: commit,
|
|
1181
|
+
with_events: with_events, refresh: refresh }
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
def change_object(variable, set: {}, commit: false, with_events: true,
|
|
1185
|
+
refresh: false, &block)
|
|
1186
|
+
_acts << { type: :change_object, variable: variable.to_s,
|
|
1187
|
+
members: _build_members(set, &block), commit: commit,
|
|
1188
|
+
with_events: with_events, refresh: refresh }
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
def retrieve_objects(entity, as:, xpath: nil, limit: nil, single: false, sort: [])
|
|
1192
|
+
_acts << { type: :retrieve_objects, entity: entity.to_s, variable: as.to_s,
|
|
1193
|
+
xpath: xpath&.to_s, limit: limit, single: single,
|
|
1194
|
+
sortings: Array(sort) }
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
def retrieve_association(from, association:, as:)
|
|
1198
|
+
_acts << {
|
|
1199
|
+
type: :retrieve_association, variable: as.to_s,
|
|
1200
|
+
start_variable: from.to_s, association: association.to_s
|
|
1201
|
+
}
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
def commit(variable, with_events: true, refresh: false)
|
|
1205
|
+
_acts << {
|
|
1206
|
+
type: :commit, variable: variable.to_s,
|
|
1207
|
+
with_events: with_events, refresh: refresh
|
|
1208
|
+
}
|
|
1209
|
+
end
|
|
1210
|
+
|
|
1211
|
+
def delete(variable, refresh: false)
|
|
1212
|
+
_acts << { type: :delete_object, variable: variable.to_s, refresh: refresh }
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
def call_microflow(name, as: nil, pass: {}, result_name: nil, use_return: nil)
|
|
1216
|
+
mappings = pass.map { |param, var| { param: param.to_s, value: var } }
|
|
1217
|
+
_acts << {
|
|
1218
|
+
type: :call_microflow, name: name.to_s, variable: as&.to_s,
|
|
1219
|
+
result_name: result_name&.to_s,
|
|
1220
|
+
use_return: use_return.nil? ? !as.nil? : use_return == true,
|
|
1221
|
+
mappings: mappings
|
|
1222
|
+
}
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
%i[java javascript nanoflow app_service].each do |runtime|
|
|
1226
|
+
define_method(:"call_#{runtime}") do |name, as: nil, pass: {},
|
|
1227
|
+
result_name: nil, use_return: nil|
|
|
1228
|
+
mappings = pass.map { |param, value| { param: param.to_s, value: value } }
|
|
1229
|
+
_acts << {
|
|
1230
|
+
type: :"call_#{runtime}", name: name.to_s,
|
|
1231
|
+
variable: as&.to_s, result_name: result_name&.to_s,
|
|
1232
|
+
use_return: use_return.nil? ? !as.nil? : use_return == true,
|
|
1233
|
+
mappings: mappings
|
|
1234
|
+
}
|
|
1235
|
+
end
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
def create_variable(name, type: nil, value: nil)
|
|
1239
|
+
_acts << {
|
|
1240
|
+
type: :create_variable, variable: name.to_s,
|
|
1241
|
+
variable_type: type&.to_s, value: value
|
|
1242
|
+
}
|
|
1243
|
+
end
|
|
1244
|
+
|
|
1245
|
+
def change_variable(name, to:)
|
|
1246
|
+
_acts << { type: :change_variable, variable: name.to_s, value: to }
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
def show_message(text = nil, type: :information, blocking: false,
|
|
1250
|
+
translations: nil, parameters: [])
|
|
1251
|
+
_acts << {
|
|
1252
|
+
type: :show_message, text: text.to_s, message_type: type.to_s,
|
|
1253
|
+
blocking: blocking,
|
|
1254
|
+
translations: translations&.transform_keys(&:to_s),
|
|
1255
|
+
parameters: Array(parameters)
|
|
1256
|
+
}
|
|
1257
|
+
end
|
|
1258
|
+
|
|
1259
|
+
def log_message(message, level: :info, node: nil, include_stack: false, parameters: [])
|
|
1260
|
+
_acts << {
|
|
1261
|
+
type: :log_message, message: message.to_s, level: level.to_s,
|
|
1262
|
+
node: node&.to_s, include_stack: include_stack,
|
|
1263
|
+
parameters: Array(parameters)
|
|
1264
|
+
}
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
def show_page(page, object: nil, location: nil, pass: {}, close_pages: nil,
|
|
1268
|
+
title: nil)
|
|
1269
|
+
_acts << {
|
|
1270
|
+
type: :show_page, page: page.to_s, variable: object&.to_s,
|
|
1271
|
+
location: location&.to_s,
|
|
1272
|
+
mappings: pass.map { |parameter, value| { parameter: parameter.to_s, value: value } },
|
|
1273
|
+
close_pages: close_pages, title: title
|
|
1274
|
+
}
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
def close_page(count: nil)
|
|
1278
|
+
_acts << { type: :close_page, count: count }
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
def aggregate(list, function:, as:, attribute: nil)
|
|
1282
|
+
_acts << {
|
|
1283
|
+
type: :aggregate, variable: list.to_s, function: function.to_s,
|
|
1284
|
+
output: as.to_s, attribute: attribute.to_s
|
|
1285
|
+
}
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
def rollback(variable, refresh: false)
|
|
1289
|
+
_acts << { type: :rollback, variable: variable.to_s, refresh: refresh }
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
def cast(variable)
|
|
1293
|
+
_acts << { type: :cast, variable: variable.to_s }
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
def create_list(entity, as:)
|
|
1297
|
+
_acts << { type: :create_list, entity: entity.to_s, variable: as.to_s }
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
def list_operation(operation, list, as:, with: nil, expression: nil)
|
|
1301
|
+
_acts << {
|
|
1302
|
+
type: :list_operation, operation: operation.to_s,
|
|
1303
|
+
variable: list.to_s, second: with&.to_s,
|
|
1304
|
+
expression: expression&.to_s, output: as.to_s
|
|
1305
|
+
}
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
def change_list(list, action:, value:)
|
|
1309
|
+
_acts << {
|
|
1310
|
+
type: :change_list, variable: list.to_s,
|
|
1311
|
+
action: action.to_s, value: value
|
|
1312
|
+
}
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
def validation_feedback(variable, attribute: nil, association: nil,
|
|
1316
|
+
translations: {}, parameters: [], error: :rollback)
|
|
1317
|
+
_acts << {
|
|
1318
|
+
type: :validation_feedback, variable: variable.to_s,
|
|
1319
|
+
attribute: attribute.to_s, association: association.to_s,
|
|
1320
|
+
translations: translations.transform_keys(&:to_s),
|
|
1321
|
+
parameters: Array(parameters), error: error.to_s
|
|
1322
|
+
}
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
def call_rest(method:, location:, location_parameters: [], headers: {},
|
|
1326
|
+
request_mapping: nil, request_variable: nil,
|
|
1327
|
+
result_mapping: nil, as: nil, result_entity: nil,
|
|
1328
|
+
timeout: nil, commit: :yes_without_events,
|
|
1329
|
+
error_result: :http_response, error: :rollback)
|
|
1330
|
+
_acts << {
|
|
1331
|
+
type: :call_rest, method: method.to_s, location: location.to_s,
|
|
1332
|
+
location_parameters: Array(location_parameters),
|
|
1333
|
+
headers: headers.transform_keys(&:to_s),
|
|
1334
|
+
request_mapping: request_mapping&.to_s,
|
|
1335
|
+
request_variable: request_variable&.to_s,
|
|
1336
|
+
result_mapping: result_mapping&.to_s, variable: as&.to_s,
|
|
1337
|
+
result_entity: result_entity&.to_s, timeout: timeout&.to_s,
|
|
1338
|
+
commit: commit.to_s, error_result: error_result.to_s,
|
|
1339
|
+
error: error.to_s
|
|
1340
|
+
}
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
def return_value(value)
|
|
1344
|
+
expression = value.is_a?(Symbol) ? "$#{value}" : value.to_s
|
|
1345
|
+
_acts << { type: :return_event, expression: expression }
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
def end_flow
|
|
1349
|
+
_acts << { type: :return_event, expression: "" }
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
def error_event
|
|
1353
|
+
_acts << { type: :error_event }
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
def continue_loop
|
|
1357
|
+
_acts << { type: :continue_event }
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
def rescue_all(&block)
|
|
1361
|
+
rescue_builder = RescueBuilder.new
|
|
1362
|
+
rescue_builder.instance_eval(&block) if block
|
|
1363
|
+
_acts << {
|
|
1364
|
+
type: :rescue_all,
|
|
1365
|
+
activities: rescue_builder.activities
|
|
1366
|
+
}
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
def decision(condition, &block)
|
|
1370
|
+
db = DecisionBuilder.new(condition.is_a?(Hash) ? condition : condition.to_s)
|
|
1371
|
+
db.instance_eval(&block) if block
|
|
1372
|
+
_acts << db.to_h
|
|
1373
|
+
end
|
|
1374
|
+
|
|
1375
|
+
def type_decision(variable, &block)
|
|
1376
|
+
builder = TypeDecisionBuilder.new(variable.to_s)
|
|
1377
|
+
builder.instance_eval(&block) if block
|
|
1378
|
+
_acts << builder.to_h
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
def loop_over(variable, as: nil, &block)
|
|
1382
|
+
lb = LoopBuilder.new(variable.to_s, as: (as || variable).to_s)
|
|
1383
|
+
lb.instance_eval(&block) if block
|
|
1384
|
+
_acts << lb.to_h
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
def while_loop(condition, &block)
|
|
1388
|
+
lb = LoopBuilder.new(nil, as: nil)
|
|
1389
|
+
lb.instance_eval(&block) if block
|
|
1390
|
+
item = lb.to_h
|
|
1391
|
+
item[:type] = :while_loop
|
|
1392
|
+
item[:condition] = condition.to_s
|
|
1393
|
+
_acts << item
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
private
|
|
1397
|
+
|
|
1398
|
+
def _build_members(set_hash, &block)
|
|
1399
|
+
if block
|
|
1400
|
+
sb = SetBuilder.new
|
|
1401
|
+
sb.instance_eval(&block)
|
|
1402
|
+
sb.members
|
|
1403
|
+
else
|
|
1404
|
+
set_hash.map { |k, v| { attribute: k.to_s, value: v } }
|
|
1405
|
+
end
|
|
1406
|
+
end
|
|
1407
|
+
end
|
|
1408
|
+
|
|
1409
|
+
class FlowBuilder
|
|
1410
|
+
include FlowBodyDsl
|
|
1411
|
+
|
|
1412
|
+
def initialize(name, runtime:, kind:, public:)
|
|
1413
|
+
@name = name.to_s
|
|
1414
|
+
@runtime = runtime
|
|
1415
|
+
@kind = kind
|
|
1416
|
+
@public = public
|
|
1417
|
+
@parameters = []
|
|
1418
|
+
@return_type = nil
|
|
1419
|
+
@doc = ""
|
|
1420
|
+
@calls = []
|
|
1421
|
+
@repositories = []
|
|
1422
|
+
@allowed_roles = nil
|
|
1423
|
+
@body = nil
|
|
1424
|
+
@return_variable_name = nil
|
|
1425
|
+
@return_expression = nil
|
|
1426
|
+
@expected_body_fingerprint = nil
|
|
1427
|
+
@allow_concurrent_execution = nil
|
|
1428
|
+
@mark_as_used = nil
|
|
1429
|
+
@excluded = nil
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1432
|
+
def parameter(name, type:)
|
|
1433
|
+
@parameters << { name: name.to_s, type: type.is_a?(Hash) ? type : type.to_s }
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
def bson_binary(base64, subtype: :generic)
|
|
1437
|
+
BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1440
|
+
def return_type(t) = (@return_type = t.to_s)
|
|
1441
|
+
def documentation(d) = (@doc = d)
|
|
1442
|
+
def allow_concurrent_execution(value = true) = (@allow_concurrent_execution = !!value)
|
|
1443
|
+
def mark_as_used(value = true) = (@mark_as_used = !!value)
|
|
1444
|
+
def excluded(value = true) = (@excluded = !!value)
|
|
1445
|
+
|
|
1446
|
+
def allowed_roles(*roles)
|
|
1447
|
+
@allowed_roles = roles.map(&:to_s)
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
def call(microflow: nil, nanoflow: nil)
|
|
1451
|
+
choices = { microflow: microflow, nanoflow: nanoflow }.compact
|
|
1452
|
+
raise ArgumentError, "call requires exactly one target" unless choices.size == 1
|
|
1453
|
+
@calls << { kind: choices.keys.first, name: choices.values.first.to_s }
|
|
1454
|
+
end
|
|
1455
|
+
|
|
1456
|
+
def uses_repository(name)
|
|
1457
|
+
@repositories << name.to_s
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
def rescue_all(&block)
|
|
1461
|
+
rb = RescueBuilder.new
|
|
1462
|
+
rb.instance_eval(&block) if block
|
|
1463
|
+
_acts << { type: :rescue_all, activities: rb.activities }
|
|
1464
|
+
end
|
|
1465
|
+
|
|
1466
|
+
def return_value(var)
|
|
1467
|
+
if var.is_a?(Symbol)
|
|
1468
|
+
@return_variable_name = var.to_s
|
|
1469
|
+
@return_expression = "$#{var}"
|
|
1470
|
+
else
|
|
1471
|
+
@return_expression = var.to_s
|
|
1472
|
+
end
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
def body_fingerprint(value)
|
|
1476
|
+
@expected_body_fingerprint = value.to_s
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1479
|
+
def self.body_digest(body, return_expression)
|
|
1480
|
+
payload = canonical_body_value([body, return_expression])
|
|
1481
|
+
Digest::SHA256.hexdigest(JSON.generate(payload))
|
|
1482
|
+
end
|
|
1483
|
+
|
|
1484
|
+
def self.canonical_body_value(value)
|
|
1485
|
+
case value
|
|
1486
|
+
when Hash
|
|
1487
|
+
value.keys.sort_by(&:to_s).to_h do |key|
|
|
1488
|
+
[key.to_s, canonical_body_value(value[key])]
|
|
1489
|
+
end
|
|
1490
|
+
when Array
|
|
1491
|
+
value.map { canonical_body_value(_1) }
|
|
1492
|
+
when Symbol
|
|
1493
|
+
{ "__symbol__" => value.to_s }
|
|
1494
|
+
else
|
|
1495
|
+
value
|
|
1496
|
+
end
|
|
1497
|
+
end
|
|
1498
|
+
|
|
1499
|
+
def to_h
|
|
1500
|
+
current_fingerprint = self.class.body_digest(@body, @return_expression)
|
|
1501
|
+
{
|
|
1502
|
+
name: @name, runtime: @runtime, kind: @kind, public: @public,
|
|
1503
|
+
parameters: @parameters, return_type: @return_type, documentation: @doc,
|
|
1504
|
+
calls: @calls, repositories: @repositories, allowed_roles: @allowed_roles,
|
|
1505
|
+
body: @body, return_variable_name: @return_variable_name,
|
|
1506
|
+
return_expression: @return_expression,
|
|
1507
|
+
allow_concurrent_execution: @allow_concurrent_execution,
|
|
1508
|
+
mark_as_used: @mark_as_used, excluded: @excluded,
|
|
1509
|
+
preserve_native_body: !@expected_body_fingerprint.nil? &&
|
|
1510
|
+
@expected_body_fingerprint == current_fingerprint
|
|
1511
|
+
}
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
private
|
|
1515
|
+
|
|
1516
|
+
def _acts = (@body ||= [])
|
|
1517
|
+
end
|
|
1518
|
+
|
|
1519
|
+
class DecisionBuilder
|
|
1520
|
+
def initialize(condition)
|
|
1521
|
+
@condition = condition
|
|
1522
|
+
@true_branch = []
|
|
1523
|
+
@false_branch = []
|
|
1524
|
+
@branches = {}
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
def on(value, &block)
|
|
1528
|
+
bb = BranchBuilder.new
|
|
1529
|
+
bb.instance_eval(&block) if block
|
|
1530
|
+
case value
|
|
1531
|
+
when true then @true_branch = bb.activities
|
|
1532
|
+
when false then @false_branch = bb.activities
|
|
1533
|
+
end
|
|
1534
|
+
@branches[value] = bb.activities
|
|
1535
|
+
end
|
|
1536
|
+
|
|
1537
|
+
def to_h
|
|
1538
|
+
{ type: :decision, condition: @condition,
|
|
1539
|
+
true_branch: @true_branch, false_branch: @false_branch,
|
|
1540
|
+
branches: @branches }
|
|
1541
|
+
end
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
class BranchBuilder
|
|
1545
|
+
include FlowBodyDsl
|
|
1546
|
+
attr_reader :activities
|
|
1547
|
+
|
|
1548
|
+
def initialize
|
|
1549
|
+
@activities = []
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1552
|
+
private
|
|
1553
|
+
|
|
1554
|
+
def _acts = @activities
|
|
1555
|
+
end
|
|
1556
|
+
|
|
1557
|
+
class TypeDecisionBuilder
|
|
1558
|
+
def initialize(variable)
|
|
1559
|
+
@variable = variable
|
|
1560
|
+
@branches = {}
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
def on_type(type, &block)
|
|
1564
|
+
add_branch(type.to_s, &block)
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
def otherwise(&block)
|
|
1568
|
+
add_branch("", &block)
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
def to_h
|
|
1572
|
+
{ type: :inheritance_decision, variable: @variable, branches: @branches }
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
private
|
|
1576
|
+
|
|
1577
|
+
def add_branch(value, &block)
|
|
1578
|
+
branch = BranchBuilder.new
|
|
1579
|
+
branch.instance_eval(&block) if block
|
|
1580
|
+
@branches[value] = branch.activities
|
|
1581
|
+
end
|
|
1582
|
+
end
|
|
1583
|
+
|
|
1584
|
+
class LoopBuilder
|
|
1585
|
+
include FlowBodyDsl
|
|
1586
|
+
|
|
1587
|
+
def initialize(variable, as:)
|
|
1588
|
+
@variable = variable
|
|
1589
|
+
@iterator = as
|
|
1590
|
+
@activities = []
|
|
1591
|
+
end
|
|
1592
|
+
|
|
1593
|
+
def to_h
|
|
1594
|
+
{ type: :loop_over, variable: @variable, iterator: @iterator, activities: @activities }
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
private
|
|
1598
|
+
|
|
1599
|
+
def _acts = @activities
|
|
1600
|
+
end
|
|
1601
|
+
|
|
1602
|
+
class RescueBuilder
|
|
1603
|
+
include FlowBodyDsl
|
|
1604
|
+
attr_reader :activities
|
|
1605
|
+
|
|
1606
|
+
def initialize
|
|
1607
|
+
@activities = []
|
|
1608
|
+
end
|
|
1609
|
+
|
|
1610
|
+
private
|
|
1611
|
+
|
|
1612
|
+
def _acts = @activities
|
|
1613
|
+
end
|
|
1614
|
+
|
|
1615
|
+
class SetBuilder
|
|
1616
|
+
attr_reader :members
|
|
1617
|
+
|
|
1618
|
+
def initialize
|
|
1619
|
+
@members = []
|
|
1620
|
+
end
|
|
1621
|
+
|
|
1622
|
+
def set(attribute, to:)
|
|
1623
|
+
@members << { attribute: attribute.to_s, value: to }
|
|
1624
|
+
end
|
|
1625
|
+
|
|
1626
|
+
def set_association(association, to:, operation: :set)
|
|
1627
|
+
@members << {
|
|
1628
|
+
association: association.to_s, value: to,
|
|
1629
|
+
operation: operation.to_s
|
|
1630
|
+
}
|
|
1631
|
+
end
|
|
1632
|
+
end
|
|
1633
|
+
|
|
1634
|
+
MicroflowBuilder = FlowBuilder
|
|
1635
|
+
end
|
|
1636
|
+
end
|