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,343 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class MDLGenerator
|
|
5
|
+
TYPE_NAMES = {
|
|
6
|
+
"autonumber" => "AutoNumber",
|
|
7
|
+
"binary" => "Binary",
|
|
8
|
+
"boolean" => "Boolean",
|
|
9
|
+
"datetime" => "DateTime",
|
|
10
|
+
"decimal" => "Decimal",
|
|
11
|
+
"hash_string" => "HashedString",
|
|
12
|
+
"integer" => "Integer",
|
|
13
|
+
"long" => "Long",
|
|
14
|
+
"string" => "String"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def self.generate(
|
|
18
|
+
model,
|
|
19
|
+
skip_associations: [],
|
|
20
|
+
skip_module_roles: [],
|
|
21
|
+
skip_user_roles: [],
|
|
22
|
+
include_modules: false,
|
|
23
|
+
skip_modules: []
|
|
24
|
+
)
|
|
25
|
+
new(
|
|
26
|
+
model,
|
|
27
|
+
skip_associations:,
|
|
28
|
+
skip_module_roles:,
|
|
29
|
+
skip_user_roles:,
|
|
30
|
+
include_modules:,
|
|
31
|
+
skip_modules:
|
|
32
|
+
).generate
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.microflow_statement(app_module, microflow)
|
|
36
|
+
new(nil).send(:microflow_statement, app_module, microflow)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.page_statement(app_module, page)
|
|
40
|
+
new(nil).send(:page_statement, app_module, page)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(
|
|
44
|
+
model,
|
|
45
|
+
skip_associations: [],
|
|
46
|
+
skip_module_roles: [],
|
|
47
|
+
skip_user_roles: [],
|
|
48
|
+
include_modules: false,
|
|
49
|
+
skip_modules: []
|
|
50
|
+
)
|
|
51
|
+
@model = model
|
|
52
|
+
@skip_associations = skip_associations
|
|
53
|
+
@skip_module_roles = skip_module_roles
|
|
54
|
+
@skip_user_roles = skip_user_roles
|
|
55
|
+
@include_modules = include_modules
|
|
56
|
+
@skip_modules = skip_modules
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def generate
|
|
60
|
+
statements = []
|
|
61
|
+
if @include_modules
|
|
62
|
+
statements.concat(@model.modules.filter_map do |app_module|
|
|
63
|
+
next if @skip_modules.include?(app_module.name)
|
|
64
|
+
|
|
65
|
+
"CREATE MODULE #{identifier(app_module.name)};"
|
|
66
|
+
end)
|
|
67
|
+
end
|
|
68
|
+
statements.concat(@model.modules.flat_map do |app_module|
|
|
69
|
+
enumerations = app_module.enumerations.map do |enumeration|
|
|
70
|
+
enumeration_statement(app_module, enumeration)
|
|
71
|
+
end
|
|
72
|
+
microflows = app_module.microflows.map do |microflow|
|
|
73
|
+
microflow_statement(app_module, microflow)
|
|
74
|
+
end
|
|
75
|
+
pages = app_module.pages.map { |page| page_statement(app_module, page) }
|
|
76
|
+
module_roles = app_module.module_roles.filter_map do |role|
|
|
77
|
+
name = "#{app_module.name}.#{role.name}"
|
|
78
|
+
module_role_statement(app_module, role) unless @skip_module_roles.include?(name)
|
|
79
|
+
end
|
|
80
|
+
entities = app_module.entities.map { |entity| entity_statement(app_module, entity) }
|
|
81
|
+
associations = app_module.entities.flat_map do |entity|
|
|
82
|
+
entity.associations.map do |association|
|
|
83
|
+
next if @skip_associations.include?(
|
|
84
|
+
"#{app_module.name}.#{association.name}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
association_statement(app_module, entity, association)
|
|
88
|
+
end.compact
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
module_roles + enumerations + entities + associations + microflows + pages
|
|
92
|
+
end)
|
|
93
|
+
|
|
94
|
+
statements.concat(user_role_statements)
|
|
95
|
+
statements.concat(project_security_statements)
|
|
96
|
+
statements.concat(navigation_statements)
|
|
97
|
+
statements.join("\n\n") << "\n"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def enumeration_statement(app_module, enumeration)
|
|
103
|
+
values = enumeration.values.map do |value|
|
|
104
|
+
" #{identifier(value.name)} '#{escape_string(value.caption)}'"
|
|
105
|
+
end
|
|
106
|
+
folder = if enumeration.folder
|
|
107
|
+
" FOLDER '#{escape_string(enumeration.folder)}'"
|
|
108
|
+
else
|
|
109
|
+
""
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
"CREATE OR MODIFY ENUMERATION #{qualified(app_module.name, enumeration.name)} (\n" \
|
|
113
|
+
"#{values.join(",\n")}\n" \
|
|
114
|
+
")#{folder};"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def microflow_statement(app_module, microflow)
|
|
118
|
+
name = qualified(app_module.name, microflow.name)
|
|
119
|
+
parameters = microflow.parameters.map do |parameter|
|
|
120
|
+
" $#{identifier(parameter.name)}: #{qualified_or_scalar(parameter.type)}"
|
|
121
|
+
end
|
|
122
|
+
returns = microflow.return_type ? "\nRETURNS #{qualified_or_scalar(microflow.return_type)}" : ""
|
|
123
|
+
folder = microflow.folder ? "\nFOLDER '#{escape_string(microflow.folder)}'" : ""
|
|
124
|
+
body = microflow.body.lines.map { |line| " #{line.rstrip}" }.join("\n")
|
|
125
|
+
grants = microflow.execute_roles.map do |role|
|
|
126
|
+
"GRANT EXECUTE ON MICROFLOW #{name} TO #{qualified_reference(role)};"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
statement = "CREATE OR MODIFY MICROFLOW #{name} (\n" \
|
|
130
|
+
"#{parameters.join(",\n")}\n" \
|
|
131
|
+
")#{returns}#{folder}\n" \
|
|
132
|
+
"BEGIN\n#{body}\nEND;"
|
|
133
|
+
grants.empty? ? statement : "#{statement}\n\n#{grants.join("\n")}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def page_statement(app_module, page)
|
|
137
|
+
name = qualified(app_module.name, page.name)
|
|
138
|
+
settings = [
|
|
139
|
+
"Title: '#{escape_string(page.title)}'",
|
|
140
|
+
"Layout: #{qualified_reference(page.layout)}"
|
|
141
|
+
]
|
|
142
|
+
settings << "Folder: '#{escape_string(page.folder)}'" if page.folder
|
|
143
|
+
unless page.parameters.empty?
|
|
144
|
+
parameters = page.parameters.map do |parameter|
|
|
145
|
+
"$#{identifier(parameter.name)}: #{qualified_reference(parameter.type)}"
|
|
146
|
+
end
|
|
147
|
+
settings << "Params: { #{parameters.join(', ')} }"
|
|
148
|
+
end
|
|
149
|
+
content = page.content.lines.map { |line| " #{line.rstrip}" }.join("\n")
|
|
150
|
+
grants = page.view_roles.map do |role|
|
|
151
|
+
"GRANT VIEW ON PAGE #{name} TO #{qualified_reference(role)};"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
statement = "CREATE OR MODIFY PAGE #{name} (\n" \
|
|
155
|
+
" #{settings.join(",\n ")}\n" \
|
|
156
|
+
") {\n#{content}\n}"
|
|
157
|
+
grants.empty? ? statement : "#{statement}\n\n#{grants.join("\n")}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def module_role_statement(app_module, role)
|
|
161
|
+
statement = "CREATE MODULE ROLE #{qualified(app_module.name, role.name)}"
|
|
162
|
+
if role.description
|
|
163
|
+
statement << " DESCRIPTION '#{escape_string(role.description)}'"
|
|
164
|
+
end
|
|
165
|
+
"#{statement};"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def user_role_statements
|
|
169
|
+
@model.user_roles.filter_map do |role|
|
|
170
|
+
next if @skip_user_roles.include?(role.name)
|
|
171
|
+
|
|
172
|
+
management = role.manage_all_roles ? " MANAGE ALL ROLES" : ""
|
|
173
|
+
module_roles = role.module_roles.map { |name| qualified_reference(name) }
|
|
174
|
+
"CREATE USER ROLE #{identifier(role.name)} " \
|
|
175
|
+
"(#{module_roles.join(', ')})#{management};"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def project_security_statements
|
|
180
|
+
security = @model.project_security
|
|
181
|
+
return [] unless security
|
|
182
|
+
|
|
183
|
+
[
|
|
184
|
+
"ALTER PROJECT SECURITY LEVEL #{security.level.upcase};",
|
|
185
|
+
"ALTER PROJECT SECURITY DEMO USERS #{security.demo_users ? 'ON' : 'OFF'};"
|
|
186
|
+
]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def navigation_statements
|
|
190
|
+
@model.navigation_profiles.map do |profile|
|
|
191
|
+
lines = [
|
|
192
|
+
"CREATE OR REPLACE NAVIGATION #{identifier(profile.name)}",
|
|
193
|
+
" HOME PAGE #{qualified_reference(profile.home_page)}"
|
|
194
|
+
]
|
|
195
|
+
profile.role_home_pages.each do |entry|
|
|
196
|
+
lines << " HOME PAGE #{qualified_reference(entry.page)} " \
|
|
197
|
+
"FOR #{qualified_reference(entry.role)}"
|
|
198
|
+
end
|
|
199
|
+
if profile.login_page
|
|
200
|
+
lines << " LOGIN PAGE #{qualified_reference(profile.login_page)}"
|
|
201
|
+
end
|
|
202
|
+
if profile.not_found_page
|
|
203
|
+
lines << " NOT FOUND PAGE #{qualified_reference(profile.not_found_page)}"
|
|
204
|
+
end
|
|
205
|
+
unless profile.items.empty?
|
|
206
|
+
lines << " MENU ("
|
|
207
|
+
profile.items.each do |item|
|
|
208
|
+
lines.concat(navigation_item_lines(item, 4))
|
|
209
|
+
end
|
|
210
|
+
lines << " )"
|
|
211
|
+
end
|
|
212
|
+
"#{lines.join("\n")};"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def navigation_item_lines(item, indentation)
|
|
217
|
+
prefix = " " * indentation
|
|
218
|
+
if item.is_a?(Model::NavigationMenu)
|
|
219
|
+
lines = ["#{prefix}MENU '#{escape_string(item.caption)}' ("]
|
|
220
|
+
item.items.each do |child|
|
|
221
|
+
lines.concat(navigation_item_lines(child, indentation + 2))
|
|
222
|
+
end
|
|
223
|
+
lines << "#{prefix});"
|
|
224
|
+
lines
|
|
225
|
+
else
|
|
226
|
+
[
|
|
227
|
+
"#{prefix}MENU ITEM '#{escape_string(item.caption)}' " \
|
|
228
|
+
"#{item.action.upcase} #{qualified_reference(item.target)};"
|
|
229
|
+
]
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def entity_statement(app_module, entity)
|
|
234
|
+
persistence = entity.persistable ? "PERSISTENT" : "NON-PERSISTENT"
|
|
235
|
+
qualified_name = qualified(app_module.name, entity.name)
|
|
236
|
+
|
|
237
|
+
attributes = entity.attributes.map do |attribute|
|
|
238
|
+
" #{identifier(attribute.name)}: #{attribute_type(attribute)}" \
|
|
239
|
+
"#{constraints(attribute)}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
statement = "CREATE OR MODIFY #{persistence} ENTITY #{qualified_name} (\n" \
|
|
243
|
+
"#{attributes.join(",\n")}\n" \
|
|
244
|
+
");"
|
|
245
|
+
grants = entity.access_rules.map do |rule|
|
|
246
|
+
entity_access_statement(qualified_name, rule)
|
|
247
|
+
end
|
|
248
|
+
grants.empty? ? statement : "#{statement}\n\n#{grants.join("\n")}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def entity_access_statement(entity_name, rule)
|
|
252
|
+
permissions = []
|
|
253
|
+
permissions << "CREATE" if rule.create
|
|
254
|
+
permissions << "DELETE" if rule.delete
|
|
255
|
+
permissions << "READ #{access_attribute_list(rule.read)}" unless rule.read.empty?
|
|
256
|
+
permissions << "WRITE #{access_attribute_list(rule.write)}" unless rule.write.empty?
|
|
257
|
+
raise ValidationError, "entity access rule requires at least one permission" if permissions.empty?
|
|
258
|
+
|
|
259
|
+
xpath = rule.xpath ? " WHERE '#{escape_string(rule.xpath)}'" : ""
|
|
260
|
+
"GRANT #{qualified_reference(rule.role)} ON #{entity_name} " \
|
|
261
|
+
"(#{permissions.join(', ')})#{xpath};"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def access_attribute_list(value)
|
|
265
|
+
return "*" if value == "*"
|
|
266
|
+
|
|
267
|
+
"(#{value.map { |name| identifier(name) }.join(', ')})"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def association_statement(app_module, entity, association)
|
|
271
|
+
association_name = qualified(app_module.name, association.name)
|
|
272
|
+
source = qualified(app_module.name, entity.name)
|
|
273
|
+
target = qualified_reference(association.target)
|
|
274
|
+
type = association.cardinality == "many" ? "ReferenceSet" : "Reference"
|
|
275
|
+
owner = association.owner == "both" ? "Both" : "Default"
|
|
276
|
+
|
|
277
|
+
"CREATE ASSOCIATION #{association_name}\n" \
|
|
278
|
+
" FROM #{source}\n" \
|
|
279
|
+
" TO #{target}\n" \
|
|
280
|
+
" TYPE #{type}\n" \
|
|
281
|
+
" OWNER #{owner};"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def attribute_type(attribute)
|
|
285
|
+
if attribute.type == "enumeration"
|
|
286
|
+
return "Enumeration(#{qualified_reference(attribute.options.fetch(:enumeration))})"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
if attribute.type == "string"
|
|
290
|
+
length = attribute.options[:length]
|
|
291
|
+
return "String(#{length || 'unlimited'})"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
TYPE_NAMES.fetch(attribute.type) do
|
|
295
|
+
raise ValidationError, "cannot generate MDL type #{attribute.type.inspect}"
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def constraints(attribute)
|
|
300
|
+
parts = []
|
|
301
|
+
parts << "NOT NULL" if attribute.required
|
|
302
|
+
parts << "DEFAULT #{literal(attribute.default)}" unless attribute.default.nil?
|
|
303
|
+
parts.empty? ? "" : " #{parts.join(' ')}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def literal(value)
|
|
307
|
+
case value
|
|
308
|
+
when true then "TRUE"
|
|
309
|
+
when false then "FALSE"
|
|
310
|
+
when Numeric then value.to_s
|
|
311
|
+
when String then "'#{value.gsub("'", "''")}'"
|
|
312
|
+
else
|
|
313
|
+
raise ValidationError, "unsupported default value: #{value.inspect}"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def escape_string(value)
|
|
318
|
+
value.to_s.gsub("'", "''")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def qualified(module_name, item_name)
|
|
322
|
+
"#{identifier(module_name)}.#{identifier(item_name)}"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def qualified_reference(value)
|
|
326
|
+
parts = value.to_s.split(".", 2)
|
|
327
|
+
raise ValidationError, "expected qualified reference, got #{value.inspect}" if parts.length != 2
|
|
328
|
+
|
|
329
|
+
qualified(*parts)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def qualified_or_scalar(value)
|
|
333
|
+
value.to_s.include?(".") ? qualified_reference(value) : identifier(value)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def identifier(value)
|
|
337
|
+
string = value.to_s
|
|
338
|
+
return string if string.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
339
|
+
|
|
340
|
+
%("#{string.gsub('"', '""')}")
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class MicroflowParser
|
|
5
|
+
class << self
|
|
6
|
+
def parse(description)
|
|
7
|
+
mdl = description.fetch("mdl")
|
|
8
|
+
header = mdl.match(
|
|
9
|
+
/\b(?:microflow|nanoflow)\s+(?<name>[\w.]+)\s*\((?<parameters>.*?)\)\s*(?:returns\s+(?<returns>\S+))?/im
|
|
10
|
+
)
|
|
11
|
+
body = mdl[/\nbegin\s*\n(?<body>.*)\nend;\s*(?:\n|$)/im, :body]
|
|
12
|
+
|
|
13
|
+
return { "mdl" => mdl, "parse_status" => "unsupported" } unless header && body
|
|
14
|
+
|
|
15
|
+
activities = parse_activities(body)
|
|
16
|
+
microflow_calls = calls(mdl, "microflow")
|
|
17
|
+
nanoflow_calls = calls(mdl, "nanoflow")
|
|
18
|
+
javascript_action_calls = calls(mdl, "javascript action")
|
|
19
|
+
{
|
|
20
|
+
"parameters" => parse_parameters(header[:parameters]),
|
|
21
|
+
"return_type" => header[:returns],
|
|
22
|
+
"folder" => mdl[/^folder\s+'([^']*)'/i, 1],
|
|
23
|
+
"variables" => activities.filter_map { |activity| activity["result_variable"] }.uniq,
|
|
24
|
+
"activities" => activities,
|
|
25
|
+
"calls" => all_calls(mdl),
|
|
26
|
+
"microflow_calls" => microflow_calls,
|
|
27
|
+
"nanoflow_calls" => nanoflow_calls,
|
|
28
|
+
"javascript_action_calls" => javascript_action_calls,
|
|
29
|
+
"execute_roles" => mdl.scan(
|
|
30
|
+
/^grant\s+execute\s+on\s+microflow\s+[\w.]+\s+to\s+([\w.]+)/i
|
|
31
|
+
).flatten,
|
|
32
|
+
"mdl" => mdl,
|
|
33
|
+
"parse_status" => "parsed"
|
|
34
|
+
}.compact
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def calls(mdl, kind)
|
|
40
|
+
mdl.scan(/\bcall\s+#{Regexp.escape(kind)}\s+([\w.]+)/i).flatten.uniq
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def all_calls(mdl)
|
|
44
|
+
mdl.scan(
|
|
45
|
+
/\bcall\s+(?:microflow|nanoflow|javascript\s+action)\s+([\w.]+)/i
|
|
46
|
+
).flatten.uniq
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_parameters(source)
|
|
50
|
+
source.lines.filter_map do |line|
|
|
51
|
+
match = line.strip.delete_suffix(",").match(/\A\$(\w+):\s*(.+)\z/)
|
|
52
|
+
{ "name" => match[1], "type" => match[2] } if match
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_activities(body)
|
|
57
|
+
activities = []
|
|
58
|
+
buffer = +""
|
|
59
|
+
|
|
60
|
+
body.each_line do |line|
|
|
61
|
+
stripped = line.strip
|
|
62
|
+
next if stripped.empty? || stripped.start_with?("@")
|
|
63
|
+
next if %w[else].include?(stripped) || stripped.match?(/\Aend\s+if;\z/i)
|
|
64
|
+
|
|
65
|
+
buffer << " " unless buffer.empty?
|
|
66
|
+
buffer << stripped
|
|
67
|
+
|
|
68
|
+
if buffer.start_with?("if ") && stripped.match?(/\bthen\z/i)
|
|
69
|
+
expression = buffer.sub(/\Aif\s+/i, "").sub(/\s+then\z/i, "")
|
|
70
|
+
activities << activity("decision", buffer, "expression" => expression)
|
|
71
|
+
buffer.clear
|
|
72
|
+
elsif stripped.end_with?(";")
|
|
73
|
+
statement = buffer.delete_suffix(";")
|
|
74
|
+
kind = activity_kind(statement)
|
|
75
|
+
activities << activity(kind, statement, activity_metadata(kind, statement))
|
|
76
|
+
buffer.clear
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
activities
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def activity_kind(statement)
|
|
84
|
+
case statement
|
|
85
|
+
when /\A(?:\$\w+\s*=\s*)?call\s+microflow\b/i then "microflow_call"
|
|
86
|
+
when /\A(?:\$\w+\s*=\s*)?call\s+nanoflow\b/i then "nanoflow_call"
|
|
87
|
+
when /\A(?:\$\w+\s*=\s*)?call\s+javascript\s+action\b/i then "javascript_action_call"
|
|
88
|
+
when /\Aretrieve\b/i then "retrieve"
|
|
89
|
+
when /\Achange\b/i then "change"
|
|
90
|
+
when /\Adelete\b/i then "delete"
|
|
91
|
+
when /\A(?:\$\w+\s*=\s*)?create\b/i then "create"
|
|
92
|
+
when /\Areturn\b/i then "return"
|
|
93
|
+
when /\Ashow\s+message\b/i then "show_message"
|
|
94
|
+
when /\Aclose\s+page\b/i then "close_page"
|
|
95
|
+
when /\Alog\b/i then "log"
|
|
96
|
+
else "statement"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def activity_metadata(kind, statement)
|
|
101
|
+
case kind
|
|
102
|
+
when "microflow_call", "nanoflow_call", "javascript_action_call"
|
|
103
|
+
call_kind = {
|
|
104
|
+
"microflow_call" => "microflow",
|
|
105
|
+
"nanoflow_call" => "nanoflow",
|
|
106
|
+
"javascript_action_call" => "javascript action"
|
|
107
|
+
}.fetch(kind)
|
|
108
|
+
{
|
|
109
|
+
"target" => statement[/\bcall\s+#{Regexp.escape(call_kind)}\s+([\w.]+)/i, 1],
|
|
110
|
+
"result_variable" => statement[/\A\$(\w+)\s*=/, 1]
|
|
111
|
+
}.compact
|
|
112
|
+
when "retrieve"
|
|
113
|
+
{
|
|
114
|
+
"result_variable" => statement[/\Aretrieve\s+\$(\w+)/i, 1],
|
|
115
|
+
"source" => statement[/\sfrom\s+(.+)\z/i, 1]
|
|
116
|
+
}.compact
|
|
117
|
+
when "create"
|
|
118
|
+
{ "result_variable" => statement[/\A\$(\w+)\s*=/, 1] }.compact
|
|
119
|
+
when "return"
|
|
120
|
+
{ "value" => statement.sub(/\Areturn\b\s*/i, "") }
|
|
121
|
+
else
|
|
122
|
+
{}
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def activity(kind, statement, metadata = {})
|
|
127
|
+
{ "kind" => kind, "statement" => statement, **metadata }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|