mendix-ruby-bridge 0.1.1
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/.mxcli-version +2 -0
- data/LICENSE +21 -0
- data/README.md +77 -0
- data/bin/git-mendix +4 -0
- data/bin/mendix-apply +145 -0
- data/bin/mendix-desktop +26 -0
- data/bin/mendix-git +121 -0
- data/bin/mendix-ruby +580 -0
- data/bin/mxcli +20 -0
- data/bin/setup-tools +25 -0
- data/lib/mendix_bridge/backend_server.rb +1404 -0
- data/lib/mendix_bridge/change_planner.rb +594 -0
- data/lib/mendix_bridge/config.rb +89 -0
- data/lib/mendix_bridge/dependency_index.rb +188 -0
- data/lib/mendix_bridge/desktop_app.rb +121 -0
- data/lib/mendix_bridge/document_parser.rb +120 -0
- data/lib/mendix_bridge/domain_parser.rb +103 -0
- data/lib/mendix_bridge/dsl.rb +540 -0
- data/lib/mendix_bridge/enumeration_parser.rb +32 -0
- data/lib/mendix_bridge/git_workflow.rb +321 -0
- data/lib/mendix_bridge/html_viewer.rb +672 -0
- data/lib/mendix_bridge/importer.rb +415 -0
- data/lib/mendix_bridge/inventory.rb +93 -0
- data/lib/mendix_bridge/mdl_generator.rb +343 -0
- data/lib/mendix_bridge/microflow_parser.rb +131 -0
- data/lib/mendix_bridge/migration.rb +290 -0
- data/lib/mendix_bridge/migration_executor.rb +234 -0
- data/lib/mendix_bridge/model.rb +204 -0
- data/lib/mendix_bridge/page_parser.rb +95 -0
- data/lib/mendix_bridge/presenter.rb +35 -0
- data/lib/mendix_bridge/project_creator.rb +181 -0
- data/lib/mendix_bridge/ruby_inventory_generator.rb +63 -0
- data/lib/mendix_bridge/security_parser.rb +72 -0
- data/lib/mendix_bridge/snapshot_diff.rb +58 -0
- data/lib/mendix_bridge/validator.rb +46 -0
- data/lib/mendix_bridge/version.rb +5 -0
- data/lib/mendix_bridge/visual_entity_plan.rb +176 -0
- data/lib/mendix_bridge.rb +127 -0
- data/share/applications/mendix-ruby-bridge.desktop +12 -0
- data/share/icons/hicolor/128x128/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/256x256/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/32x32/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/48x48/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/512x512/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/64x64/apps/mendix-ruby-bridge.png +0 -0
- data/web/dist/apple-touch-icon.png +0 -0
- data/web/dist/assets/index-BO6iPT1P.css +1 -0
- data/web/dist/assets/index-JGODw81W.js +27 -0
- data/web/dist/brand/mendix-ruby-bridge.png +0 -0
- data/web/dist/favicon.png +0 -0
- data/web/dist/icons.svg +24 -0
- data/web/dist/index.html +17 -0
- metadata +179 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
module Model
|
|
5
|
+
Attribute = Data.define(:name, :type, :required, :default, :options) do
|
|
6
|
+
def to_h
|
|
7
|
+
{ name:, type:, required:, default:, **options }.compact
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Association = Data.define(:name, :target, :cardinality, :owner) do
|
|
12
|
+
def to_h
|
|
13
|
+
{ name:, target:, cardinality:, owner: }.compact
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
AccessRule = Data.define(:role, :create, :delete, :read, :write, :xpath) do
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
role:,
|
|
21
|
+
create:,
|
|
22
|
+
delete:,
|
|
23
|
+
read:,
|
|
24
|
+
write:,
|
|
25
|
+
xpath:
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Entity = Data.define(
|
|
31
|
+
:name,
|
|
32
|
+
:persistable,
|
|
33
|
+
:attributes,
|
|
34
|
+
:associations,
|
|
35
|
+
:access_rules
|
|
36
|
+
) do
|
|
37
|
+
def to_h
|
|
38
|
+
{
|
|
39
|
+
name:,
|
|
40
|
+
persistable:,
|
|
41
|
+
attributes: attributes.map(&:to_h),
|
|
42
|
+
associations: associations.map(&:to_h),
|
|
43
|
+
access_rules: access_rules.map(&:to_h)
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
EnumerationValue = Data.define(:name, :caption) do
|
|
49
|
+
def to_h
|
|
50
|
+
{ name:, caption: }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Enumeration = Data.define(:name, :folder, :values) do
|
|
55
|
+
def to_h
|
|
56
|
+
{ name:, folder:, values: values.map(&:to_h) }.compact
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
FlowParameter = Data.define(:name, :type) do
|
|
61
|
+
def to_h
|
|
62
|
+
{ name:, type: }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Microflow = Data.define(
|
|
67
|
+
:name,
|
|
68
|
+
:parameters,
|
|
69
|
+
:return_type,
|
|
70
|
+
:folder,
|
|
71
|
+
:body,
|
|
72
|
+
:execute_roles
|
|
73
|
+
) do
|
|
74
|
+
def to_h
|
|
75
|
+
{
|
|
76
|
+
name:,
|
|
77
|
+
parameters: parameters.map(&:to_h),
|
|
78
|
+
return_type:,
|
|
79
|
+
folder:,
|
|
80
|
+
body:,
|
|
81
|
+
execute_roles:
|
|
82
|
+
}.compact
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Page = Data.define(
|
|
87
|
+
:name,
|
|
88
|
+
:title,
|
|
89
|
+
:layout,
|
|
90
|
+
:folder,
|
|
91
|
+
:parameters,
|
|
92
|
+
:content,
|
|
93
|
+
:view_roles
|
|
94
|
+
) do
|
|
95
|
+
def to_h
|
|
96
|
+
{
|
|
97
|
+
name:,
|
|
98
|
+
title:,
|
|
99
|
+
layout:,
|
|
100
|
+
folder:,
|
|
101
|
+
parameters: parameters.map(&:to_h),
|
|
102
|
+
content:,
|
|
103
|
+
view_roles:
|
|
104
|
+
}.compact
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
ModuleRole = Data.define(:name, :description) do
|
|
109
|
+
def to_h
|
|
110
|
+
{ name:, description: }.compact
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
UserRole = Data.define(:name, :module_roles, :manage_all_roles) do
|
|
115
|
+
def to_h
|
|
116
|
+
{ name:, module_roles:, manage_all_roles: }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
ProjectSecurity = Data.define(:level, :demo_users) do
|
|
121
|
+
def to_h
|
|
122
|
+
{ level:, demo_users: }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
NavigationItem = Data.define(:caption, :action, :target) do
|
|
127
|
+
def to_h
|
|
128
|
+
{ caption:, action:, target: }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
NavigationMenu = Data.define(:caption, :items) do
|
|
133
|
+
def to_h
|
|
134
|
+
{ caption:, items: items.map(&:to_h) }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
RoleHomePage = Data.define(:role, :page) do
|
|
139
|
+
def to_h
|
|
140
|
+
{ role:, page: }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
NavigationProfile = Data.define(
|
|
145
|
+
:name,
|
|
146
|
+
:home_page,
|
|
147
|
+
:role_home_pages,
|
|
148
|
+
:login_page,
|
|
149
|
+
:not_found_page,
|
|
150
|
+
:items
|
|
151
|
+
) do
|
|
152
|
+
def to_h
|
|
153
|
+
{
|
|
154
|
+
name:,
|
|
155
|
+
home_page:,
|
|
156
|
+
role_home_pages: role_home_pages.map(&:to_h),
|
|
157
|
+
login_page:,
|
|
158
|
+
not_found_page:,
|
|
159
|
+
items: items.map(&:to_h)
|
|
160
|
+
}.compact
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
AppModule = Data.define(
|
|
165
|
+
:name,
|
|
166
|
+
:entities,
|
|
167
|
+
:enumerations,
|
|
168
|
+
:microflows,
|
|
169
|
+
:pages,
|
|
170
|
+
:module_roles
|
|
171
|
+
) do
|
|
172
|
+
def to_h
|
|
173
|
+
{
|
|
174
|
+
name:,
|
|
175
|
+
entities: entities.map(&:to_h),
|
|
176
|
+
enumerations: enumerations.map(&:to_h),
|
|
177
|
+
microflows: microflows.map(&:to_h),
|
|
178
|
+
pages: pages.map(&:to_h),
|
|
179
|
+
module_roles: module_roles.map(&:to_h)
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
App = Data.define(
|
|
185
|
+
:name,
|
|
186
|
+
:version,
|
|
187
|
+
:modules,
|
|
188
|
+
:user_roles,
|
|
189
|
+
:project_security,
|
|
190
|
+
:navigation_profiles
|
|
191
|
+
) do
|
|
192
|
+
def to_h
|
|
193
|
+
{
|
|
194
|
+
schema_version: 1,
|
|
195
|
+
app: { name:, version: }.compact,
|
|
196
|
+
modules: modules.map(&:to_h),
|
|
197
|
+
user_roles: user_roles.map(&:to_h),
|
|
198
|
+
project_security: project_security&.to_h,
|
|
199
|
+
navigation_profiles: navigation_profiles.map(&:to_h)
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class PageParser
|
|
5
|
+
class << self
|
|
6
|
+
def parse(description)
|
|
7
|
+
mdl = description.fetch("mdl")
|
|
8
|
+
header = mdl.match(
|
|
9
|
+
/\bpage\s+(?<name>[\w.]+)\s*\((?<settings>.*?)\)\s*\{/im
|
|
10
|
+
)
|
|
11
|
+
return { "mdl" => mdl, "parse_status" => "unsupported" } unless header
|
|
12
|
+
|
|
13
|
+
settings = header[:settings]
|
|
14
|
+
actions = mdl.scan(/\bAction:\s*([^,\n\)]+)/i).flatten.map(&:strip)
|
|
15
|
+
data_sources = mdl.scan(/\bDataSource:\s*([^,\n\)]+)/i).flatten.map(&:strip)
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
"title" => quoted_setting(settings, "Title"),
|
|
19
|
+
"layout" => scalar_setting(settings, "Layout"),
|
|
20
|
+
"folder" => quoted_setting(settings, "Folder"),
|
|
21
|
+
"parameters" => parse_parameters(settings),
|
|
22
|
+
"widgets" => parse_widgets(mdl),
|
|
23
|
+
"widget_types" => widget_counts(mdl),
|
|
24
|
+
"data_sources" => data_sources.uniq,
|
|
25
|
+
"actions" => actions.map { |action| parse_action(action) },
|
|
26
|
+
"microflow_calls" => referenced(actions, "microflow"),
|
|
27
|
+
"nanoflow_calls" => referenced(actions, "nanoflow"),
|
|
28
|
+
"page_links" => referenced(actions, "show_page"),
|
|
29
|
+
"attributes" => mdl.scan(/\bAttribute:\s*([^,\n\)]+)/i).flatten.map(&:strip).uniq,
|
|
30
|
+
"view_roles" => mdl.scan(
|
|
31
|
+
/^grant\s+view\s+on\s+page\s+[\w.]+\s+to\s+([\w.]+)/i
|
|
32
|
+
).flatten,
|
|
33
|
+
"mdl" => mdl,
|
|
34
|
+
"parse_status" => "parsed"
|
|
35
|
+
}.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def quoted_setting(settings, name)
|
|
41
|
+
settings[/\b#{Regexp.escape(name)}:\s*'((?:''|[^'])*)'/i, 1]&.gsub("''", "'")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def scalar_setting(settings, name)
|
|
45
|
+
settings[/\b#{Regexp.escape(name)}:\s*([^,\n]+)/i, 1]&.strip
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_parameters(settings)
|
|
49
|
+
source = settings[/\bParams:\s*\{(.*?)\}/im, 1]
|
|
50
|
+
return [] unless source
|
|
51
|
+
|
|
52
|
+
source.scan(/\$(\w+):\s*([\w.]+)/).map do |name, type|
|
|
53
|
+
{ "name" => name, "type" => type }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_widgets(mdl)
|
|
58
|
+
mdl.lines.filter_map do |line|
|
|
59
|
+
stripped = line.strip
|
|
60
|
+
next if stripped.start_with?("--")
|
|
61
|
+
|
|
62
|
+
match = stripped.match(
|
|
63
|
+
/\A(?<type>[a-z][a-z0-9_]*)\s*(?<name>"[^"]+"|[A-Za-z_]\w*)?\s*(?<opening>\(|\{)/
|
|
64
|
+
)
|
|
65
|
+
next unless match
|
|
66
|
+
next if %w[create grant].include?(match[:type])
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
"type" => match[:type],
|
|
70
|
+
"name" => match[:name]&.delete_prefix('"')&.delete_suffix('"'),
|
|
71
|
+
"declaration" => stripped
|
|
72
|
+
}.compact
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def widget_counts(mdl)
|
|
77
|
+
parse_widgets(mdl).each_with_object(Hash.new(0)) do |widget, counts|
|
|
78
|
+
counts[widget["type"]] += 1
|
|
79
|
+
end.sort.to_h
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def parse_action(action)
|
|
83
|
+
kind, target = action.split(/\s+/, 2)
|
|
84
|
+
{ "kind" => kind, "target" => target }.compact
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def referenced(actions, kind)
|
|
88
|
+
actions.filter_map do |action|
|
|
89
|
+
match = action.match(/\A#{Regexp.escape(kind)}\s+([\w.]+)/i)
|
|
90
|
+
match[1] if match
|
|
91
|
+
end.uniq
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class Presenter
|
|
5
|
+
def self.summary(element)
|
|
6
|
+
details = element.details&.reject { |key, _value| %w[mdl raw].include?(key) }
|
|
7
|
+
{
|
|
8
|
+
"name" => element.qualified_name || element.label,
|
|
9
|
+
"type" => element.type,
|
|
10
|
+
"path" => element.path,
|
|
11
|
+
"details" => details
|
|
12
|
+
}.compact
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.search_result(element)
|
|
16
|
+
{
|
|
17
|
+
"name" => element.qualified_name || element.label,
|
|
18
|
+
"type" => element.type,
|
|
19
|
+
"path" => element.path.join(" / ")
|
|
20
|
+
}.compact
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.text(element)
|
|
24
|
+
result = summary(element)
|
|
25
|
+
lines = [
|
|
26
|
+
"Name: #{result.fetch('name')}",
|
|
27
|
+
"Type: #{result.fetch('type')}",
|
|
28
|
+
"Path: #{element.path.join(' / ')}"
|
|
29
|
+
]
|
|
30
|
+
details = result["details"]
|
|
31
|
+
lines << "Details:\n#{JSON.pretty_generate(details)}" if details && !details.empty?
|
|
32
|
+
lines.join("\n")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
|
|
7
|
+
module MendixBridge
|
|
8
|
+
class ProjectCreationError < StandardError; end
|
|
9
|
+
|
|
10
|
+
class ProjectCreator
|
|
11
|
+
Result = Data.define(:project_dir, :project_file, :model_file, :git_initialized)
|
|
12
|
+
|
|
13
|
+
GITIGNORE = <<~IGNORE
|
|
14
|
+
deployment/
|
|
15
|
+
releases/
|
|
16
|
+
testresults/
|
|
17
|
+
*.mpr.bak
|
|
18
|
+
*.mpr.backup
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
IGNORE
|
|
22
|
+
|
|
23
|
+
def initialize(mxcli:)
|
|
24
|
+
@mxcli = File.expand_path(mxcli)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create(model, source_file:, output_dir:, version:, git: true)
|
|
28
|
+
source_file = File.expand_path(source_file)
|
|
29
|
+
output_dir = File.expand_path(output_dir)
|
|
30
|
+
validate_inputs!(source_file, output_dir, version)
|
|
31
|
+
|
|
32
|
+
run!(
|
|
33
|
+
@mxcli,
|
|
34
|
+
"new",
|
|
35
|
+
model.name,
|
|
36
|
+
"--version", version,
|
|
37
|
+
"--output-dir", output_dir,
|
|
38
|
+
"--skip-init",
|
|
39
|
+
error_prefix: "mxcli could not create the blank project"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
project_file = locate_project_file(output_dir)
|
|
43
|
+
existing_modules = read_modules(project_file)
|
|
44
|
+
mdl = MendixBridge.compile(
|
|
45
|
+
model,
|
|
46
|
+
format: :mdl,
|
|
47
|
+
include_modules: true,
|
|
48
|
+
skip_modules: existing_modules
|
|
49
|
+
)
|
|
50
|
+
apply_model!(mdl, project_file)
|
|
51
|
+
check_project!(project_file, version)
|
|
52
|
+
|
|
53
|
+
model_file = File.join(output_dir, "app.rb")
|
|
54
|
+
FileUtils.cp(source_file, model_file)
|
|
55
|
+
File.write(File.join(output_dir, ".mendix-version"), "#{version}\n")
|
|
56
|
+
File.write(File.join(output_dir, ".gitignore"), GITIGNORE)
|
|
57
|
+
initialize_git!(output_dir) if git
|
|
58
|
+
|
|
59
|
+
Result.new(
|
|
60
|
+
project_dir: output_dir,
|
|
61
|
+
project_file:,
|
|
62
|
+
model_file:,
|
|
63
|
+
git_initialized: git
|
|
64
|
+
)
|
|
65
|
+
rescue ProjectCreationError
|
|
66
|
+
raise
|
|
67
|
+
rescue StandardError => error
|
|
68
|
+
raise ProjectCreationError, error.message
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def validate_inputs!(source_file, output_dir, version)
|
|
74
|
+
raise ProjectCreationError, "Ruby model does not exist: #{source_file}" unless
|
|
75
|
+
File.file?(source_file)
|
|
76
|
+
raise ProjectCreationError, "mxcli is not executable: #{@mxcli}" unless
|
|
77
|
+
File.executable?(@mxcli)
|
|
78
|
+
raise ProjectCreationError, "output directory already exists: #{output_dir}" if
|
|
79
|
+
File.exist?(output_dir)
|
|
80
|
+
raise ProjectCreationError, "invalid Mendix version: #{version}" unless
|
|
81
|
+
version.to_s.match?(/\A\d+\.\d+\.\d+\z/)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def locate_project_file(output_dir)
|
|
85
|
+
projects = Dir[File.join(output_dir, "*.mpr")]
|
|
86
|
+
unless projects.length == 1
|
|
87
|
+
raise ProjectCreationError,
|
|
88
|
+
"expected one .mpr in #{output_dir}, found #{projects.length}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
projects.first
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def read_modules(project_file)
|
|
95
|
+
output = run!(
|
|
96
|
+
@mxcli,
|
|
97
|
+
"-p", project_file,
|
|
98
|
+
"-c", "SHOW MODULES;",
|
|
99
|
+
error_prefix: "could not inspect modules in the blank project"
|
|
100
|
+
)
|
|
101
|
+
output.scan(/^\|\s+([A-Za-z_][A-Za-z0-9_]*)\s+\|/).flatten
|
|
102
|
+
.reject { |name| name == "Module" }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def apply_model!(mdl, project_file)
|
|
106
|
+
Tempfile.create(["mendix-ruby-new-", ".mdl"]) do |file|
|
|
107
|
+
file.write(mdl)
|
|
108
|
+
file.flush
|
|
109
|
+
run!(
|
|
110
|
+
@mxcli,
|
|
111
|
+
"check", file.path,
|
|
112
|
+
error_prefix: "generated project MDL is invalid"
|
|
113
|
+
)
|
|
114
|
+
run!(
|
|
115
|
+
@mxcli,
|
|
116
|
+
"exec", file.path,
|
|
117
|
+
"-p", project_file,
|
|
118
|
+
error_prefix: "mxcli could not apply the Ruby model"
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def check_project!(project_file, version)
|
|
124
|
+
mx = File.join(
|
|
125
|
+
Dir.home,
|
|
126
|
+
".mxcli",
|
|
127
|
+
"mxbuild",
|
|
128
|
+
version,
|
|
129
|
+
"modeler",
|
|
130
|
+
"mx"
|
|
131
|
+
)
|
|
132
|
+
raise ProjectCreationError, "Mendix mx #{version} is not installed: #{mx}" unless
|
|
133
|
+
File.executable?(mx)
|
|
134
|
+
|
|
135
|
+
run!(
|
|
136
|
+
mx,
|
|
137
|
+
"check",
|
|
138
|
+
project_file,
|
|
139
|
+
chdir: File.dirname(project_file),
|
|
140
|
+
error_prefix: "official Mendix consistency check failed"
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def initialize_git!(output_dir)
|
|
145
|
+
run!(
|
|
146
|
+
"git",
|
|
147
|
+
"init",
|
|
148
|
+
"-b", "main",
|
|
149
|
+
chdir: output_dir,
|
|
150
|
+
error_prefix: "could not initialize Git"
|
|
151
|
+
)
|
|
152
|
+
run!(
|
|
153
|
+
"git",
|
|
154
|
+
"add",
|
|
155
|
+
".",
|
|
156
|
+
chdir: output_dir,
|
|
157
|
+
error_prefix: "could not stage the initial project"
|
|
158
|
+
)
|
|
159
|
+
run!(
|
|
160
|
+
"git",
|
|
161
|
+
"commit",
|
|
162
|
+
"-m", "Initial Setup",
|
|
163
|
+
chdir: output_dir,
|
|
164
|
+
error_prefix: "could not create the initial Git commit"
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def run!(*command, chdir: nil, error_prefix:)
|
|
169
|
+
stdout, stderr, status = if chdir
|
|
170
|
+
Open3.capture3(*command, chdir:)
|
|
171
|
+
else
|
|
172
|
+
Open3.capture3(*command)
|
|
173
|
+
end
|
|
174
|
+
return stdout if status.success?
|
|
175
|
+
|
|
176
|
+
detail = stderr.strip
|
|
177
|
+
detail = stdout.strip if detail.empty?
|
|
178
|
+
raise ProjectCreationError, "#{error_prefix}: #{detail}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class RubyInventoryGenerator
|
|
5
|
+
def self.generate(inventory)
|
|
6
|
+
inventory.modules.each_with_object({}) do |app_module, result|
|
|
7
|
+
name = app_module.qualified_name
|
|
8
|
+
elements = inventory.elements.select do |element|
|
|
9
|
+
element.qualified_name&.start_with?("#{name}.") &&
|
|
10
|
+
%w[entity association].include?(element.type)
|
|
11
|
+
end
|
|
12
|
+
next if elements.empty?
|
|
13
|
+
|
|
14
|
+
result[name] = {
|
|
15
|
+
"entities" => elements.select { |element| element.type == "entity" }.map { |element| serialize(element) },
|
|
16
|
+
"associations" => elements.select { |element| element.type == "association" }.map { |element| serialize(element) }
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.ruby_source(module_name, domain)
|
|
22
|
+
namespace = module_name.gsub(/[^A-Za-z0-9_]/, "_")
|
|
23
|
+
json = JSON.pretty_generate(domain)
|
|
24
|
+
|
|
25
|
+
<<~RUBY
|
|
26
|
+
# frozen_string_literal: true
|
|
27
|
+
# Generated by mendix-ruby import. Changes will be replaced by refresh.
|
|
28
|
+
|
|
29
|
+
require "json"
|
|
30
|
+
|
|
31
|
+
module MendixGenerated
|
|
32
|
+
module #{namespace}
|
|
33
|
+
DOMAIN_MODEL = JSON.parse(<<~JSON)
|
|
34
|
+
#{indent(json, 6)}
|
|
35
|
+
JSON
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
RUBY
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.file_name(module_name)
|
|
42
|
+
module_name
|
|
43
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
44
|
+
.gsub(/[^A-Za-z0-9]+/, "_")
|
|
45
|
+
.downcase
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.serialize(element)
|
|
49
|
+
{
|
|
50
|
+
"name" => element.label,
|
|
51
|
+
"qualified_name" => element.qualified_name,
|
|
52
|
+
"details" => element.details
|
|
53
|
+
}.compact
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.indent(text, spaces)
|
|
57
|
+
prefix = " " * spaces
|
|
58
|
+
text.lines.map { |line| "#{prefix}#{line}" }.join
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private_class_method :serialize, :indent
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class SecurityParser
|
|
5
|
+
class << self
|
|
6
|
+
def parse(type, description)
|
|
7
|
+
case type
|
|
8
|
+
when "projectsecurity" then parse_project_security(description)
|
|
9
|
+
when "modulerole" then parse_module_role(description)
|
|
10
|
+
when "userrole" then parse_user_role(description)
|
|
11
|
+
else raise ArgumentError, "unsupported security element: #{type}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def parse_project_security(description)
|
|
18
|
+
settings = description.to_h do |entry|
|
|
19
|
+
[entry.fetch("Property"), typed_value(entry["Value"])]
|
|
20
|
+
end
|
|
21
|
+
{
|
|
22
|
+
"settings" => settings,
|
|
23
|
+
"parse_status" => "parsed",
|
|
24
|
+
"raw" => description
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_module_role(description)
|
|
29
|
+
mdl = description.fetch("mdl")
|
|
30
|
+
{
|
|
31
|
+
"description" => mdl[
|
|
32
|
+
/\bmodule\s+role\s+[\w.]+\s+description\s+'((?:''|[^'])*)'/i, 1
|
|
33
|
+
]&.gsub("''", "'"),
|
|
34
|
+
"included_in_user_roles" => mdl[
|
|
35
|
+
/--\s*Included in user roles:\s*(.+)$/i, 1
|
|
36
|
+
]&.split(",")&.map(&:strip) || [],
|
|
37
|
+
"mdl" => mdl,
|
|
38
|
+
"parse_status" => "parsed"
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse_user_role(description)
|
|
43
|
+
mdl = description.fetch("mdl")
|
|
44
|
+
declaration = mdl.match(
|
|
45
|
+
/create\s+user\s+role\s+(?<name>\w+)\s*\((?<module_roles>.*?)\)(?<management>.*?);/im
|
|
46
|
+
)
|
|
47
|
+
return { "mdl" => mdl, "parse_status" => "unsupported" } unless declaration
|
|
48
|
+
|
|
49
|
+
management = declaration[:management].strip
|
|
50
|
+
{
|
|
51
|
+
"module_roles" => declaration[:module_roles].split(",").map(&:strip),
|
|
52
|
+
"manage_all_roles" => management.match?(/\bmanage\s+all\s+roles\b/i),
|
|
53
|
+
"manageable_roles" => management[
|
|
54
|
+
/\bmanage\s+roles\s*\((.*?)\)/i, 1
|
|
55
|
+
]&.split(",")&.map(&:strip) || [],
|
|
56
|
+
"check_security" => mdl.match?(/--\s*Check security:\s*enabled/i),
|
|
57
|
+
"mdl" => mdl,
|
|
58
|
+
"parse_status" => "parsed"
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def typed_value(value)
|
|
63
|
+
case value
|
|
64
|
+
when "true" then true
|
|
65
|
+
when "false" then false
|
|
66
|
+
when /\A\d+\z/ then value.to_i
|
|
67
|
+
else value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class SnapshotDiff
|
|
5
|
+
Result = Data.define(:added, :modified, :removed) do
|
|
6
|
+
def empty?
|
|
7
|
+
added.empty? && modified.empty? && removed.empty?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_h
|
|
11
|
+
{ added:, modified:, removed: }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.compare(before, after)
|
|
16
|
+
old_elements = index(before)
|
|
17
|
+
new_elements = index(after)
|
|
18
|
+
old_keys = old_elements.keys
|
|
19
|
+
new_keys = new_elements.keys
|
|
20
|
+
|
|
21
|
+
Result.new(
|
|
22
|
+
added: describe(new_keys - old_keys, new_elements),
|
|
23
|
+
modified: describe(
|
|
24
|
+
(old_keys & new_keys).select do |key|
|
|
25
|
+
canonical(old_elements.fetch(key)) != canonical(new_elements.fetch(key))
|
|
26
|
+
end,
|
|
27
|
+
new_elements
|
|
28
|
+
),
|
|
29
|
+
removed: describe(old_keys - new_keys, old_elements)
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.index(inventory)
|
|
34
|
+
inventory.elements.to_h do |element|
|
|
35
|
+
identity = element.qualified_name || element.path.join("/")
|
|
36
|
+
["#{element.type}:#{identity}", element]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.describe(keys, elements)
|
|
41
|
+
keys.sort.map do |key|
|
|
42
|
+
element = elements.fetch(key)
|
|
43
|
+
{
|
|
44
|
+
"id" => key,
|
|
45
|
+
"type" => element.type,
|
|
46
|
+
"name" => element.qualified_name || element.label,
|
|
47
|
+
"path" => element.path
|
|
48
|
+
}.compact
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.canonical(element)
|
|
53
|
+
JSON.generate(element.to_h)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private_class_method :index, :describe, :canonical
|
|
57
|
+
end
|
|
58
|
+
end
|