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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.mxcli-version +2 -0
  3. data/LICENSE +21 -0
  4. data/README.md +77 -0
  5. data/bin/git-mendix +4 -0
  6. data/bin/mendix-apply +145 -0
  7. data/bin/mendix-desktop +26 -0
  8. data/bin/mendix-git +121 -0
  9. data/bin/mendix-ruby +580 -0
  10. data/bin/mxcli +20 -0
  11. data/bin/setup-tools +25 -0
  12. data/lib/mendix_bridge/backend_server.rb +1404 -0
  13. data/lib/mendix_bridge/change_planner.rb +594 -0
  14. data/lib/mendix_bridge/config.rb +89 -0
  15. data/lib/mendix_bridge/dependency_index.rb +188 -0
  16. data/lib/mendix_bridge/desktop_app.rb +121 -0
  17. data/lib/mendix_bridge/document_parser.rb +120 -0
  18. data/lib/mendix_bridge/domain_parser.rb +103 -0
  19. data/lib/mendix_bridge/dsl.rb +540 -0
  20. data/lib/mendix_bridge/enumeration_parser.rb +32 -0
  21. data/lib/mendix_bridge/git_workflow.rb +321 -0
  22. data/lib/mendix_bridge/html_viewer.rb +672 -0
  23. data/lib/mendix_bridge/importer.rb +415 -0
  24. data/lib/mendix_bridge/inventory.rb +93 -0
  25. data/lib/mendix_bridge/mdl_generator.rb +343 -0
  26. data/lib/mendix_bridge/microflow_parser.rb +131 -0
  27. data/lib/mendix_bridge/migration.rb +290 -0
  28. data/lib/mendix_bridge/migration_executor.rb +234 -0
  29. data/lib/mendix_bridge/model.rb +204 -0
  30. data/lib/mendix_bridge/page_parser.rb +95 -0
  31. data/lib/mendix_bridge/presenter.rb +35 -0
  32. data/lib/mendix_bridge/project_creator.rb +181 -0
  33. data/lib/mendix_bridge/ruby_inventory_generator.rb +63 -0
  34. data/lib/mendix_bridge/security_parser.rb +72 -0
  35. data/lib/mendix_bridge/snapshot_diff.rb +58 -0
  36. data/lib/mendix_bridge/validator.rb +46 -0
  37. data/lib/mendix_bridge/version.rb +5 -0
  38. data/lib/mendix_bridge/visual_entity_plan.rb +176 -0
  39. data/lib/mendix_bridge.rb +127 -0
  40. data/share/applications/mendix-ruby-bridge.desktop +12 -0
  41. data/share/icons/hicolor/128x128/apps/mendix-ruby-bridge.png +0 -0
  42. data/share/icons/hicolor/256x256/apps/mendix-ruby-bridge.png +0 -0
  43. data/share/icons/hicolor/32x32/apps/mendix-ruby-bridge.png +0 -0
  44. data/share/icons/hicolor/48x48/apps/mendix-ruby-bridge.png +0 -0
  45. data/share/icons/hicolor/512x512/apps/mendix-ruby-bridge.png +0 -0
  46. data/share/icons/hicolor/64x64/apps/mendix-ruby-bridge.png +0 -0
  47. data/web/dist/apple-touch-icon.png +0 -0
  48. data/web/dist/assets/index-BO6iPT1P.css +1 -0
  49. data/web/dist/assets/index-JGODw81W.js +27 -0
  50. data/web/dist/brand/mendix-ruby-bridge.png +0 -0
  51. data/web/dist/favicon.png +0 -0
  52. data/web/dist/icons.svg +24 -0
  53. data/web/dist/index.html +17 -0
  54. metadata +179 -0
@@ -0,0 +1,540 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MendixBridge
4
+ module DSL
5
+ class RootBuilder
6
+ def self.build(&block)
7
+ builder = new
8
+ builder.instance_eval(&block)
9
+ builder.result
10
+ end
11
+
12
+ def app(name, version: nil, &block)
13
+ raise ArgumentError, "only one app can be declared" if @app
14
+
15
+ @app = AppBuilder.build(name, version:, &block)
16
+ end
17
+
18
+ def result
19
+ @app || raise(ArgumentError, "an app declaration is required")
20
+ end
21
+ end
22
+
23
+ class AppBuilder
24
+ def self.build(name, version: nil, &block)
25
+ builder = new(name, version)
26
+ builder.instance_eval(&block) if block
27
+ builder.result
28
+ end
29
+
30
+ def initialize(name, version)
31
+ @name = name.to_s
32
+ @version = version&.to_s
33
+ @modules = []
34
+ @user_roles = []
35
+ @project_security = nil
36
+ @navigation_profiles = []
37
+ end
38
+
39
+ def modulo(name, &block)
40
+ add_module(name, &block)
41
+ end
42
+
43
+ def app_module(name, &block)
44
+ add_module(name, &block)
45
+ end
46
+
47
+ def user_role(name, module_roles:, manage_all_roles: false)
48
+ raise ArgumentError, "duplicate user role: #{name}" if
49
+ @user_roles.any? { |role| role.name == name.to_s }
50
+
51
+ roles = Array(module_roles).map(&:to_s)
52
+ raise ArgumentError, "user role #{name} requires module roles" if roles.empty?
53
+
54
+ @user_roles << Model::UserRole.new(
55
+ name: name.to_s,
56
+ module_roles: roles,
57
+ manage_all_roles:
58
+ )
59
+ end
60
+
61
+ def project_security(level:, demo_users: false)
62
+ raise ArgumentError, "project security is already declared" if @project_security
63
+
64
+ normalized_level = level.to_s.downcase
65
+ unless %w[off prototype production].include?(normalized_level)
66
+ raise ArgumentError, "security level must be off, prototype, or production"
67
+ end
68
+
69
+ @project_security = Model::ProjectSecurity.new(
70
+ level: normalized_level,
71
+ demo_users: !!demo_users
72
+ )
73
+ end
74
+
75
+ def navigation_profile(
76
+ name,
77
+ home_page:,
78
+ login_page: nil,
79
+ not_found_page: nil,
80
+ &block
81
+ )
82
+ raise ArgumentError, "duplicate navigation profile: #{name}" if
83
+ @navigation_profiles.any? { |profile| profile.name == name.to_s }
84
+
85
+ @navigation_profiles << NavigationBuilder.build(
86
+ name,
87
+ home_page:,
88
+ login_page:,
89
+ not_found_page:,
90
+ &block
91
+ )
92
+ end
93
+
94
+ def result
95
+ Model::App.new(
96
+ name: @name,
97
+ version: @version,
98
+ modules: @modules,
99
+ user_roles: @user_roles,
100
+ project_security: @project_security,
101
+ navigation_profiles: @navigation_profiles
102
+ )
103
+ end
104
+
105
+ private
106
+
107
+ def add_module(name, &block)
108
+ ensure_unique!(@modules, name, "module")
109
+ @modules << ModuleBuilder.build(name, &block)
110
+ end
111
+
112
+ def ensure_unique!(items, name, kind)
113
+ return unless items.any? { |item| item.name == name.to_s }
114
+
115
+ raise ArgumentError, "duplicate #{kind}: #{name}"
116
+ end
117
+ end
118
+
119
+ class NavigationBuilder
120
+ def self.build(name, home_page:, login_page: nil, not_found_page: nil, &block)
121
+ builder = new(name, home_page, login_page, not_found_page)
122
+ builder.instance_eval(&block) if block
123
+ builder.result
124
+ end
125
+
126
+ def initialize(name, home_page, login_page, not_found_page)
127
+ @name = name.to_s
128
+ @home_page = home_page.to_s
129
+ @login_page = login_page&.to_s
130
+ @not_found_page = not_found_page&.to_s
131
+ @role_home_pages = []
132
+ @items = []
133
+ end
134
+
135
+ def home_page(page, for_role:)
136
+ role = for_role.to_s
137
+ raise ArgumentError, "duplicate role home page: #{role}" if
138
+ @role_home_pages.any? { |entry| entry.role == role }
139
+
140
+ @role_home_pages << Model::RoleHomePage.new(role:, page: page.to_s)
141
+ end
142
+
143
+ def menu_item(caption, page: nil, microflow: nil, nanoflow: nil)
144
+ @items << build_item(caption, page:, microflow:, nanoflow:)
145
+ end
146
+
147
+ def menu(caption, &block)
148
+ builder = NavigationMenuBuilder.new
149
+ builder.instance_eval(&block)
150
+ @items << Model::NavigationMenu.new(
151
+ caption: caption.to_s,
152
+ items: builder.items
153
+ )
154
+ end
155
+
156
+ def result
157
+ Model::NavigationProfile.new(
158
+ name: @name,
159
+ home_page: @home_page,
160
+ role_home_pages: @role_home_pages,
161
+ login_page: @login_page,
162
+ not_found_page: @not_found_page,
163
+ items: @items
164
+ )
165
+ end
166
+
167
+ private
168
+
169
+ def build_item(caption, page:, microflow:, nanoflow:)
170
+ targets = { "page" => page, "microflow" => microflow, "nanoflow" => nanoflow }
171
+ .reject { |_action, target| target.nil? }
172
+ raise ArgumentError, "menu item requires exactly one target" unless targets.length == 1
173
+
174
+ action, target = targets.first
175
+ Model::NavigationItem.new(
176
+ caption: caption.to_s,
177
+ action:,
178
+ target: target.to_s
179
+ )
180
+ end
181
+ end
182
+
183
+ class NavigationMenuBuilder
184
+ attr_reader :items
185
+
186
+ def initialize
187
+ @items = []
188
+ end
189
+
190
+ def menu_item(caption, page: nil, microflow: nil, nanoflow: nil)
191
+ targets = { "page" => page, "microflow" => microflow, "nanoflow" => nanoflow }
192
+ .reject { |_action, target| target.nil? }
193
+ raise ArgumentError, "menu item requires exactly one target" unless targets.length == 1
194
+
195
+ action, target = targets.first
196
+ @items << Model::NavigationItem.new(
197
+ caption: caption.to_s,
198
+ action:,
199
+ target: target.to_s
200
+ )
201
+ end
202
+ end
203
+
204
+ class ModuleBuilder
205
+ def self.build(name, &block)
206
+ builder = new(name)
207
+ builder.instance_eval(&block) if block
208
+ builder.result
209
+ end
210
+
211
+ def initialize(name)
212
+ @name = name.to_s
213
+ @entities = []
214
+ @enumerations = []
215
+ @microflows = []
216
+ @pages = []
217
+ @module_roles = []
218
+ end
219
+
220
+ def entity(name, persistable: true, &block)
221
+ if @entities.any? { |entity| entity.name == name.to_s }
222
+ raise ArgumentError, "duplicate entity: #{name}"
223
+ end
224
+
225
+ @entities << EntityBuilder.build(name, persistable:, &block)
226
+ end
227
+
228
+ def enumeration(name, folder: nil, &block)
229
+ if @enumerations.any? { |enumeration| enumeration.name == name.to_s }
230
+ raise ArgumentError, "duplicate enumeration: #{name}"
231
+ end
232
+
233
+ @enumerations << EnumerationBuilder.build(name, folder:, &block)
234
+ end
235
+
236
+ def microflow(name, returns: nil, folder: nil, &block)
237
+ if @microflows.any? { |microflow| microflow.name == name.to_s }
238
+ raise ArgumentError, "duplicate microflow: #{name}"
239
+ end
240
+
241
+ @microflows << MicroflowBuilder.build(
242
+ name,
243
+ returns:,
244
+ folder:,
245
+ &block
246
+ )
247
+ end
248
+
249
+ def page(name, title:, layout:, folder: nil, &block)
250
+ raise ArgumentError, "duplicate page: #{name}" if
251
+ @pages.any? { |page| page.name == name.to_s }
252
+
253
+ @pages << PageBuilder.build(
254
+ name,
255
+ title:,
256
+ layout:,
257
+ folder:,
258
+ &block
259
+ )
260
+ end
261
+
262
+ def module_role(name, description: nil)
263
+ raise ArgumentError, "duplicate module role: #{name}" if
264
+ @module_roles.any? { |role| role.name == name.to_s }
265
+
266
+ @module_roles << Model::ModuleRole.new(
267
+ name: name.to_s,
268
+ description: description&.to_s
269
+ )
270
+ end
271
+
272
+ def result
273
+ Model::AppModule.new(
274
+ name: @name,
275
+ entities: @entities,
276
+ enumerations: @enumerations,
277
+ microflows: @microflows,
278
+ pages: @pages,
279
+ module_roles: @module_roles
280
+ )
281
+ end
282
+ end
283
+
284
+ class PageBuilder
285
+ def self.build(name, title:, layout:, folder: nil, &block)
286
+ builder = new(name, title, layout, folder)
287
+ builder.instance_eval(&block) if block
288
+ builder.result
289
+ end
290
+
291
+ def initialize(name, title, layout, folder)
292
+ @name = name.to_s
293
+ @title = title.to_s
294
+ @layout = layout.to_s
295
+ @folder = folder&.to_s
296
+ @parameters = []
297
+ @content = nil
298
+ @view_roles = []
299
+ end
300
+
301
+ def parameter(name, type)
302
+ if @parameters.any? { |parameter| parameter.name == name.to_s }
303
+ raise ArgumentError, "duplicate page parameter: #{name}"
304
+ end
305
+
306
+ @parameters << Model::FlowParameter.new(name: name.to_s, type: type.to_s)
307
+ end
308
+
309
+ def content(source)
310
+ raise ArgumentError, "page content is already declared" if @content
311
+
312
+ @content = source.to_s.strip
313
+ end
314
+
315
+ def view_role(role)
316
+ role = role.to_s
317
+ raise ArgumentError, "duplicate view role: #{role}" if @view_roles.include?(role)
318
+
319
+ @view_roles << role
320
+ end
321
+
322
+ def result
323
+ raise ArgumentError, "page #{@name} requires content" if @content.nil? || @content.empty?
324
+
325
+ Model::Page.new(
326
+ name: @name,
327
+ title: @title,
328
+ layout: @layout,
329
+ folder: @folder,
330
+ parameters: @parameters,
331
+ content: @content,
332
+ view_roles: @view_roles
333
+ )
334
+ end
335
+ end
336
+
337
+ class MicroflowBuilder
338
+ def self.build(name, returns: nil, folder: nil, &block)
339
+ builder = new(name, returns, folder)
340
+ builder.instance_eval(&block) if block
341
+ builder.result
342
+ end
343
+
344
+ def initialize(name, returns, folder)
345
+ @name = name.to_s
346
+ @return_type = returns&.to_s
347
+ @folder = folder&.to_s
348
+ @parameters = []
349
+ @body = nil
350
+ @execute_roles = []
351
+ end
352
+
353
+ def parameter(name, type)
354
+ if @parameters.any? { |parameter| parameter.name == name.to_s }
355
+ raise ArgumentError, "duplicate microflow parameter: #{name}"
356
+ end
357
+
358
+ @parameters << Model::FlowParameter.new(name: name.to_s, type: type.to_s)
359
+ end
360
+
361
+ def body(source)
362
+ raise ArgumentError, "microflow body is already declared" if @body
363
+
364
+ @body = source.to_s.strip
365
+ end
366
+
367
+ def execute_role(role)
368
+ role = role.to_s
369
+ raise ArgumentError, "duplicate execute role: #{role}" if @execute_roles.include?(role)
370
+
371
+ @execute_roles << role
372
+ end
373
+
374
+ def result
375
+ raise ArgumentError, "microflow #{@name} requires a body" if @body.nil? || @body.empty?
376
+
377
+ Model::Microflow.new(
378
+ name: @name,
379
+ parameters: @parameters,
380
+ return_type: @return_type,
381
+ folder: @folder,
382
+ body: @body,
383
+ execute_roles: @execute_roles
384
+ )
385
+ end
386
+ end
387
+
388
+ class EnumerationBuilder
389
+ def self.build(name, folder: nil, &block)
390
+ builder = new(name, folder)
391
+ builder.instance_eval(&block) if block
392
+ builder.result
393
+ end
394
+
395
+ def initialize(name, folder)
396
+ @name = name.to_s
397
+ @folder = folder&.to_s
398
+ @values = []
399
+ end
400
+
401
+ def value(name, caption: nil)
402
+ if @values.any? { |value| value.name == name.to_s }
403
+ raise ArgumentError, "duplicate enumeration value: #{name}"
404
+ end
405
+
406
+ @values << Model::EnumerationValue.new(
407
+ name: name.to_s,
408
+ caption: (caption || name).to_s
409
+ )
410
+ end
411
+
412
+ def result
413
+ raise ArgumentError, "enumeration #{@name} requires at least one value" if @values.empty?
414
+
415
+ Model::Enumeration.new(name: @name, folder: @folder, values: @values)
416
+ end
417
+ end
418
+
419
+ class EntityBuilder
420
+ CARDINALITIES = %i[one many].freeze
421
+ ATTRIBUTE_TYPES = %i[
422
+ autonumber binary boolean datetime decimal enumeration
423
+ hash_string integer long string
424
+ ].freeze
425
+
426
+ def self.build(name, persistable: true, &block)
427
+ builder = new(name, persistable)
428
+ builder.instance_eval(&block) if block
429
+ builder.result
430
+ end
431
+
432
+ def initialize(name, persistable)
433
+ @name = name.to_s
434
+ @persistable = persistable
435
+ @attributes = []
436
+ @associations = []
437
+ @access_rules = []
438
+ end
439
+
440
+ def attribute(
441
+ name,
442
+ type,
443
+ required: false,
444
+ default: nil,
445
+ length: 200,
446
+ enumeration: nil
447
+ )
448
+ type = type.to_sym
449
+ unless ATTRIBUTE_TYPES.include?(type)
450
+ raise ArgumentError,
451
+ "unsupported attribute type #{type.inspect}; " \
452
+ "expected one of: #{ATTRIBUTE_TYPES.join(', ')}"
453
+ end
454
+
455
+ ensure_unique!(@attributes, name, "attribute")
456
+ if type == :enumeration && enumeration.nil?
457
+ raise ArgumentError, "enumeration attributes require enumeration: \"Module.Name\""
458
+ end
459
+
460
+ options = case type
461
+ when :string
462
+ { length: }
463
+ when :enumeration
464
+ { enumeration: enumeration.to_s }
465
+ else
466
+ {}
467
+ end
468
+
469
+ @attributes << Model::Attribute.new(
470
+ name: name.to_s,
471
+ type: type.to_s,
472
+ required:,
473
+ default:,
474
+ options:
475
+ )
476
+ end
477
+
478
+ def association(name, target:, cardinality: :one, owner: :default)
479
+ cardinality = cardinality.to_sym
480
+ unless CARDINALITIES.include?(cardinality)
481
+ raise ArgumentError, "cardinality must be :one or :many"
482
+ end
483
+
484
+ ensure_unique!(@associations, name, "association")
485
+ @associations << Model::Association.new(
486
+ name: name.to_s,
487
+ target: target.to_s,
488
+ cardinality: cardinality.to_s,
489
+ owner: owner.to_s
490
+ )
491
+ end
492
+
493
+ def access(
494
+ role,
495
+ create: false,
496
+ delete: false,
497
+ read: [],
498
+ write: [],
499
+ where: nil
500
+ )
501
+ role = role.to_s
502
+ raise ArgumentError, "duplicate entity access role: #{role}" if
503
+ @access_rules.any? { |rule| rule.role == role }
504
+
505
+ @access_rules << Model::AccessRule.new(
506
+ role:,
507
+ create: !!create,
508
+ delete: !!delete,
509
+ read: access_attributes(read),
510
+ write: access_attributes(write),
511
+ xpath: where&.to_s
512
+ )
513
+ end
514
+
515
+ def result
516
+ Model::Entity.new(
517
+ name: @name,
518
+ persistable: @persistable,
519
+ attributes: @attributes,
520
+ associations: @associations,
521
+ access_rules: @access_rules
522
+ )
523
+ end
524
+
525
+ private
526
+
527
+ def ensure_unique!(items, name, kind)
528
+ return unless items.any? { |item| item.name == name.to_s }
529
+
530
+ raise ArgumentError, "duplicate #{kind}: #{name}"
531
+ end
532
+
533
+ def access_attributes(value)
534
+ return "*" if value == :all || value == "*"
535
+
536
+ Array(value).map(&:to_s)
537
+ end
538
+ end
539
+ end
540
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MendixBridge
4
+ class EnumerationParser
5
+ class << self
6
+ def parse(description)
7
+ mdl = description.fetch("mdl")
8
+ body = mdl[
9
+ /\benumeration\s+[\w.]+\s*\(\s*(?<body>.*?)\s*\)\s*(?:FOLDER\b|;)/im,
10
+ :body
11
+ ]
12
+
13
+ return { "mdl" => mdl, "parse_status" => "unsupported" } unless body
14
+
15
+ {
16
+ "values" => parse_values(body),
17
+ "folder" => mdl[/\bFOLDER\s+'((?:''|[^'])*)'/i, 1]&.gsub("''", "'"),
18
+ "mdl" => mdl,
19
+ "parse_status" => "parsed"
20
+ }.compact
21
+ end
22
+
23
+ private
24
+
25
+ def parse_values(body)
26
+ body.scan(/^\s*(\w+)\s+'((?:''|[^'])*)'\s*,?\s*$/).map do |name, caption|
27
+ { "name" => name, "caption" => caption.gsub("''", "'") }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end